Merge component 'cli' from git@github.com:docker/cli master
This commit is contained in:
@@ -34,6 +34,14 @@ binary: ## build executable for Linux
|
||||
cross: ## build executable for macOS and Windows
|
||||
./scripts/build/cross
|
||||
|
||||
.PHONY: binary-windows
|
||||
binary-windows: ## build executable for Windows
|
||||
./scripts/build/windows
|
||||
|
||||
.PHONY: binary-osx
|
||||
binary-osx: ## build executable for macOS
|
||||
./scripts/build/osx
|
||||
|
||||
.PHONY: dynbinary
|
||||
dynbinary: ## build dynamically linked binary
|
||||
./scripts/build/dynbinary
|
||||
|
||||
@@ -12,11 +12,14 @@ import (
|
||||
cliconfig "github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
cliflags "github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
dopts "github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-connections/sockets"
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
"github.com/docker/notary"
|
||||
notaryclient "github.com/docker/notary/client"
|
||||
"github.com/docker/notary/passphrase"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -39,6 +42,7 @@ type Cli interface {
|
||||
SetIn(in *InStream)
|
||||
ConfigFile() *configfile.ConfigFile
|
||||
ServerInfo() ServerInfo
|
||||
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
||||
}
|
||||
|
||||
// DockerCli is an instance the docker command line client.
|
||||
@@ -111,44 +115,58 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
|
||||
var err error
|
||||
cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile)
|
||||
if tlsconfig.IsErrEncryptedKey(err) {
|
||||
var (
|
||||
passwd string
|
||||
giveup bool
|
||||
)
|
||||
passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil)
|
||||
|
||||
for attempts := 0; tlsconfig.IsErrEncryptedKey(err); attempts++ {
|
||||
// some code and comments borrowed from notary/trustmanager/keystore.go
|
||||
passwd, giveup, err = passRetriever("private", "encrypted TLS private", false, attempts)
|
||||
// Check if the passphrase retriever got an error or if it is telling us to give up
|
||||
if giveup || err != nil {
|
||||
return errors.Wrap(err, "private key is encrypted, but could not get passphrase")
|
||||
}
|
||||
|
||||
opts.Common.TLSOptions.Passphrase = passwd
|
||||
cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile)
|
||||
newClient := func(password string) (client.APIClient, error) {
|
||||
opts.Common.TLSOptions.Passphrase = password
|
||||
return NewAPIClientFromFlags(opts.Common, cli.configFile)
|
||||
}
|
||||
cli.client, err = getClientWithPassword(passRetriever, newClient)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cli.initializeFromClient()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *DockerCli) initializeFromClient() {
|
||||
cli.defaultVersion = cli.client.ClientVersion()
|
||||
|
||||
if ping, err := cli.client.Ping(context.Background()); err == nil {
|
||||
cli.server = ServerInfo{
|
||||
HasExperimental: ping.Experimental,
|
||||
OSType: ping.OSType,
|
||||
}
|
||||
|
||||
cli.client.NegotiateAPIVersionPing(ping)
|
||||
} else {
|
||||
ping, err := cli.client.Ping(context.Background())
|
||||
if err != nil {
|
||||
// Default to true if we fail to connect to daemon
|
||||
cli.server = ServerInfo{HasExperimental: true}
|
||||
|
||||
if ping.APIVersion != "" {
|
||||
cli.client.NegotiateAPIVersionPing(ping)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
cli.server = ServerInfo{
|
||||
HasExperimental: ping.Experimental,
|
||||
OSType: ping.OSType,
|
||||
}
|
||||
cli.client.NegotiateAPIVersionPing(ping)
|
||||
}
|
||||
|
||||
func getClientWithPassword(passRetriever notary.PassRetriever, newClient func(password string) (client.APIClient, error)) (client.APIClient, error) {
|
||||
for attempts := 0; ; attempts++ {
|
||||
passwd, giveup, err := passRetriever("private", "encrypted TLS private", false, attempts)
|
||||
if giveup || err != nil {
|
||||
return nil, errors.Wrap(err, "private key is encrypted, but could not get passphrase")
|
||||
}
|
||||
|
||||
apiclient, err := newClient(passwd)
|
||||
if !tlsconfig.IsErrEncryptedKey(err) {
|
||||
return apiclient, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NotaryClient provides a Notary Repository to interact with signed metadata for an image
|
||||
func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) {
|
||||
return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
|
||||
}
|
||||
|
||||
// ServerInfo stores details about the supported features and platform of the
|
||||
|
||||
@@ -4,12 +4,18 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"crypto/x509"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestNewAPIClientFromFlags(t *testing.T) {
|
||||
@@ -43,7 +49,7 @@ func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
|
||||
assert.Equal(t, customVersion, apiclient.ClientVersion())
|
||||
}
|
||||
|
||||
// TODO: move to gotestyourself
|
||||
// TODO: use gotestyourself/env.Patch
|
||||
func patchEnvVariable(t *testing.T, key, value string) func() {
|
||||
oldValue, ok := os.LookupEnv(key)
|
||||
require.NoError(t, os.Setenv(key, value))
|
||||
@@ -55,3 +61,138 @@ func patchEnvVariable(t *testing.T, key, value string) func() {
|
||||
require.NoError(t, os.Setenv(key, oldValue))
|
||||
}
|
||||
}
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
pingFunc func() (types.Ping, error)
|
||||
version string
|
||||
negotiated bool
|
||||
}
|
||||
|
||||
func (c *fakeClient) Ping(_ context.Context) (types.Ping, error) {
|
||||
return c.pingFunc()
|
||||
}
|
||||
|
||||
func (c *fakeClient) ClientVersion() string {
|
||||
return c.version
|
||||
}
|
||||
|
||||
func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) {
|
||||
c.negotiated = true
|
||||
}
|
||||
|
||||
func TestInitializeFromClient(t *testing.T) {
|
||||
defaultVersion := "v1.55"
|
||||
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
pingFunc func() (types.Ping, error)
|
||||
expectedServer ServerInfo
|
||||
negotiated bool
|
||||
}{
|
||||
{
|
||||
doc: "successful ping",
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{Experimental: true, OSType: "linux", APIVersion: "v1.30"}, nil
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"},
|
||||
negotiated: true,
|
||||
},
|
||||
{
|
||||
doc: "failed ping, no API version",
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{}, errors.New("failed")
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true},
|
||||
},
|
||||
{
|
||||
doc: "failed ping, with API version",
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{APIVersion: "v1.33"}, errors.New("failed")
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true},
|
||||
negotiated: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
apiclient := &fakeClient{
|
||||
pingFunc: testcase.pingFunc,
|
||||
version: defaultVersion,
|
||||
}
|
||||
|
||||
cli := &DockerCli{client: apiclient}
|
||||
cli.initializeFromClient()
|
||||
assert.Equal(t, defaultVersion, cli.defaultVersion)
|
||||
assert.Equal(t, testcase.expectedServer, cli.server)
|
||||
assert.Equal(t, testcase.negotiated, apiclient.negotiated)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClientWithPassword(t *testing.T) {
|
||||
expected := "password"
|
||||
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
password string
|
||||
retrieverErr error
|
||||
retrieverGiveup bool
|
||||
newClientErr error
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
doc: "successful connect",
|
||||
password: expected,
|
||||
},
|
||||
{
|
||||
doc: "password retriever exhausted",
|
||||
retrieverGiveup: true,
|
||||
retrieverErr: errors.New("failed"),
|
||||
expectedErr: "private key is encrypted, but could not get passphrase",
|
||||
},
|
||||
{
|
||||
doc: "password retriever error",
|
||||
retrieverErr: errors.New("failed"),
|
||||
expectedErr: "failed",
|
||||
},
|
||||
{
|
||||
doc: "newClient error",
|
||||
newClientErr: errors.New("failed to connect"),
|
||||
expectedErr: "failed to connect",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
passRetriever := func(_, _ string, _ bool, attempts int) (passphrase string, giveup bool, err error) {
|
||||
// Always return an invalid pass first to test iteration
|
||||
switch attempts {
|
||||
case 0:
|
||||
return "something else", false, nil
|
||||
default:
|
||||
return testcase.password, testcase.retrieverGiveup, testcase.retrieverErr
|
||||
}
|
||||
}
|
||||
|
||||
newClient := func(currentPassword string) (client.APIClient, error) {
|
||||
if testcase.newClientErr != nil {
|
||||
return nil, testcase.newClientErr
|
||||
}
|
||||
if currentPassword == expected {
|
||||
return &client.Client{}, nil
|
||||
}
|
||||
return &client.Client{}, x509.IncorrectPasswordError
|
||||
}
|
||||
|
||||
_, err := getClientWithPassword(passRetriever, newClient)
|
||||
if testcase.expectedErr != "" {
|
||||
testutil.ErrorContains(t, err, testcase.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/docker/cli/cli/command/stack"
|
||||
"github.com/docker/cli/cli/command/swarm"
|
||||
"github.com/docker/cli/cli/command/system"
|
||||
"github.com/docker/cli/cli/command/trust"
|
||||
"github.com/docker/cli/cli/command/volume"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -69,6 +70,9 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
|
||||
// swarm
|
||||
swarm.NewSwarmCommand(dockerCli),
|
||||
|
||||
// trust
|
||||
trust.NewTrustCommand(dockerCli),
|
||||
|
||||
// volume
|
||||
volume.NewVolumeCommand(dockerCli),
|
||||
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
"vbom.ml/util/sortorder"
|
||||
)
|
||||
|
||||
type byConfigName []swarm.Config
|
||||
|
||||
func (r byConfigName) Len() int { return len(r) }
|
||||
func (r byConfigName) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
||||
func (r byConfigName) Less(i, j int) bool {
|
||||
return sortorder.NaturalLess(r[i].Spec.Name, r[j].Spec.Name)
|
||||
}
|
||||
|
||||
type listOptions struct {
|
||||
quiet bool
|
||||
format string
|
||||
@@ -55,6 +67,8 @@ func runConfigList(dockerCli command.Cli, options listOptions) error {
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(byConfigName(configs))
|
||||
|
||||
configCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewConfigFormat(format, options.quiet),
|
||||
|
||||
@@ -50,14 +50,20 @@ func TestConfigList(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*Config(ConfigID("ID-foo"),
|
||||
ConfigName("foo"),
|
||||
*Config(ConfigID("ID-1-foo"),
|
||||
ConfigName("1-foo"),
|
||||
ConfigVersion(swarm.Version{Index: 10}),
|
||||
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*Config(ConfigID("ID-bar"),
|
||||
ConfigName("bar"),
|
||||
*Config(ConfigID("ID-10-foo"),
|
||||
ConfigName("10-foo"),
|
||||
ConfigVersion(swarm.Version{Index: 11}),
|
||||
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*Config(ConfigID("ID-2-foo"),
|
||||
ConfigName("2-foo"),
|
||||
ConfigVersion(swarm.Version{Index: 11}),
|
||||
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
@@ -66,9 +72,8 @@ func TestConfigList(t *testing.T) {
|
||||
},
|
||||
})
|
||||
cmd := newConfigListCommand(cli)
|
||||
cmd.SetOutput(cli.OutBuffer())
|
||||
assert.NoError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "config-list.golden")
|
||||
golden.Assert(t, cli.OutBuffer().String(), "config-list-sort.golden")
|
||||
}
|
||||
|
||||
func TestConfigListWithQuietOption(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
ID NAME CREATED UPDATED
|
||||
ID-1-foo 1-foo 2 hours ago About an hour ago
|
||||
ID-2-foo 2-foo 2 hours ago About an hour ago
|
||||
ID-10-foo 10-foo 2 hours ago About an hour ago
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
foo
|
||||
bar label=label-bar
|
||||
foo
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
ID NAME CREATED UPDATED
|
||||
ID-foo foo 2 hours ago About an hour ago
|
||||
ID-bar bar 2 hours ago About an hour ago
|
||||
ID-foo foo 2 hours ago About an hour ago
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
foo
|
||||
bar label=label-bar
|
||||
foo
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
ID-foo
|
||||
ID-bar
|
||||
ID-foo
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
ID NAME CREATED UPDATED
|
||||
ID-foo foo 2 hours ago About an hour ago
|
||||
ID-bar bar 2 hours ago About an hour ago
|
||||
@@ -0,0 +1,150 @@
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTrustTagTableFormat = "table {{.SignedTag}}\t{{.Digest}}\t{{.Signers}}"
|
||||
signedTagNameHeader = "SIGNED TAG"
|
||||
trustedDigestHeader = "DIGEST"
|
||||
signersHeader = "SIGNERS"
|
||||
defaultSignerInfoTableFormat = "table {{.Signer}}\t{{.Keys}}"
|
||||
signerNameHeader = "SIGNER"
|
||||
keysHeader = "KEYS"
|
||||
)
|
||||
|
||||
// SignedTagInfo represents all formatted information needed to describe a signed tag:
|
||||
// Name: name of the signed tag
|
||||
// Digest: hex encoded digest of the contents
|
||||
// Signers: list of entities who signed the tag
|
||||
type SignedTagInfo struct {
|
||||
Name string
|
||||
Digest string
|
||||
Signers []string
|
||||
}
|
||||
|
||||
// SignerInfo represents all formatted information needed to describe a signer:
|
||||
// Name: name of the signer role
|
||||
// Keys: the keys associated with the signer
|
||||
type SignerInfo struct {
|
||||
Name string
|
||||
Keys []string
|
||||
}
|
||||
|
||||
// NewTrustTagFormat returns a Format for rendering using a trusted tag Context
|
||||
func NewTrustTagFormat() Format {
|
||||
return defaultTrustTagTableFormat
|
||||
}
|
||||
|
||||
// NewSignerInfoFormat returns a Format for rendering a signer role info Context
|
||||
func NewSignerInfoFormat() Format {
|
||||
return defaultSignerInfoTableFormat
|
||||
}
|
||||
|
||||
// TrustTagWrite writes the context
|
||||
func TrustTagWrite(ctx Context, signedTagInfoList []SignedTagInfo) error {
|
||||
render := func(format func(subContext subContext) error) error {
|
||||
for _, signedTag := range signedTagInfoList {
|
||||
if err := format(&trustTagContext{s: signedTag}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
trustTagCtx := trustTagContext{}
|
||||
trustTagCtx.header = trustTagHeaderContext{
|
||||
"SignedTag": signedTagNameHeader,
|
||||
"Digest": trustedDigestHeader,
|
||||
"Signers": signersHeader,
|
||||
}
|
||||
return ctx.Write(&trustTagCtx, render)
|
||||
}
|
||||
|
||||
type trustTagHeaderContext map[string]string
|
||||
|
||||
type trustTagContext struct {
|
||||
HeaderContext
|
||||
s SignedTagInfo
|
||||
}
|
||||
|
||||
// SignedTag returns the name of the signed tag
|
||||
func (c *trustTagContext) SignedTag() string {
|
||||
return c.s.Name
|
||||
}
|
||||
|
||||
// Digest returns the hex encoded digest associated with this signed tag
|
||||
func (c *trustTagContext) Digest() string {
|
||||
return c.s.Digest
|
||||
}
|
||||
|
||||
// Signers returns the sorted list of entities who signed this tag
|
||||
func (c *trustTagContext) Signers() string {
|
||||
sort.Strings(c.s.Signers)
|
||||
return strings.Join(c.s.Signers, ", ")
|
||||
}
|
||||
|
||||
// SignerInfoWrite writes the context
|
||||
func SignerInfoWrite(ctx Context, signerInfoList []SignerInfo) error {
|
||||
render := func(format func(subContext subContext) error) error {
|
||||
for _, signerInfo := range signerInfoList {
|
||||
if err := format(&signerInfoContext{
|
||||
trunc: ctx.Trunc,
|
||||
s: signerInfo,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
signerInfoCtx := signerInfoContext{}
|
||||
signerInfoCtx.header = signerInfoHeaderContext{
|
||||
"Signer": signerNameHeader,
|
||||
"Keys": keysHeader,
|
||||
}
|
||||
return ctx.Write(&signerInfoCtx, render)
|
||||
}
|
||||
|
||||
type signerInfoHeaderContext map[string]string
|
||||
|
||||
type signerInfoContext struct {
|
||||
HeaderContext
|
||||
trunc bool
|
||||
s SignerInfo
|
||||
}
|
||||
|
||||
// Keys returns the sorted list of keys associated with the signer
|
||||
func (c *signerInfoContext) Keys() string {
|
||||
sort.Strings(c.s.Keys)
|
||||
truncatedKeys := []string{}
|
||||
if c.trunc {
|
||||
for _, keyID := range c.s.Keys {
|
||||
truncatedKeys = append(truncatedKeys, stringid.TruncateID(keyID))
|
||||
}
|
||||
return strings.Join(truncatedKeys, ", ")
|
||||
}
|
||||
return strings.Join(c.s.Keys, ", ")
|
||||
}
|
||||
|
||||
// Signer returns the name of the signer
|
||||
func (c *signerInfoContext) Signer() string {
|
||||
return c.s.Name
|
||||
}
|
||||
|
||||
// SignerInfoList helps sort []SignerInfo by signer names
|
||||
type SignerInfoList []SignerInfo
|
||||
|
||||
func (signerInfoComp SignerInfoList) Len() int {
|
||||
return len(signerInfoComp)
|
||||
}
|
||||
|
||||
func (signerInfoComp SignerInfoList) Less(i, j int) bool {
|
||||
return signerInfoComp[i].Name < signerInfoComp[j].Name
|
||||
}
|
||||
|
||||
func (signerInfoComp SignerInfoList) Swap(i, j int) {
|
||||
signerInfoComp[i], signerInfoComp[j] = signerInfoComp[j], signerInfoComp[i]
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTrustTag(t *testing.T) {
|
||||
digest := stringid.GenerateRandomID()
|
||||
trustedTag := "tag"
|
||||
|
||||
var ctx trustTagContext
|
||||
|
||||
cases := []struct {
|
||||
trustTagCtx trustTagContext
|
||||
expValue string
|
||||
call func() string
|
||||
}{
|
||||
{
|
||||
trustTagContext{
|
||||
s: SignedTagInfo{Name: trustedTag,
|
||||
Digest: digest,
|
||||
Signers: nil,
|
||||
},
|
||||
},
|
||||
digest,
|
||||
ctx.Digest,
|
||||
},
|
||||
{
|
||||
trustTagContext{
|
||||
s: SignedTagInfo{Name: trustedTag,
|
||||
Digest: digest,
|
||||
Signers: nil,
|
||||
},
|
||||
},
|
||||
trustedTag,
|
||||
ctx.SignedTag,
|
||||
},
|
||||
// Empty signers makes a row with empty string
|
||||
{
|
||||
trustTagContext{
|
||||
s: SignedTagInfo{Name: trustedTag,
|
||||
Digest: digest,
|
||||
Signers: nil,
|
||||
},
|
||||
},
|
||||
"",
|
||||
ctx.Signers,
|
||||
},
|
||||
{
|
||||
trustTagContext{
|
||||
s: SignedTagInfo{Name: trustedTag,
|
||||
Digest: digest,
|
||||
Signers: []string{"alice", "bob", "claire"},
|
||||
},
|
||||
},
|
||||
"alice, bob, claire",
|
||||
ctx.Signers,
|
||||
},
|
||||
// alphabetic signing on Signers
|
||||
{
|
||||
trustTagContext{
|
||||
s: SignedTagInfo{Name: trustedTag,
|
||||
Digest: digest,
|
||||
Signers: []string{"claire", "bob", "alice"},
|
||||
},
|
||||
},
|
||||
"alice, bob, claire",
|
||||
ctx.Signers,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
ctx = c.trustTagCtx
|
||||
v := c.call()
|
||||
if v != c.expValue {
|
||||
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustTagContextWrite(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
context Context
|
||||
expected string
|
||||
}{
|
||||
// Errors
|
||||
{
|
||||
Context{
|
||||
Format: "{{InvalidFunction}}",
|
||||
},
|
||||
`Template parsing error: template: :1: function "InvalidFunction" not defined
|
||||
`,
|
||||
},
|
||||
{
|
||||
Context{
|
||||
Format: "{{nil}}",
|
||||
},
|
||||
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
|
||||
`,
|
||||
},
|
||||
// Table Format
|
||||
{
|
||||
Context{
|
||||
Format: NewTrustTagFormat(),
|
||||
},
|
||||
`SIGNED TAG DIGEST SIGNERS
|
||||
tag1 deadbeef alice
|
||||
tag2 aaaaaaaa alice, bob
|
||||
tag3 bbbbbbbb
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range cases {
|
||||
signedTags := []SignedTagInfo{
|
||||
{Name: "tag1", Digest: "deadbeef", Signers: []string{"alice"}},
|
||||
{Name: "tag2", Digest: "aaaaaaaa", Signers: []string{"alice", "bob"}},
|
||||
{Name: "tag3", Digest: "bbbbbbbb", Signers: []string{}},
|
||||
}
|
||||
out := bytes.NewBufferString("")
|
||||
testcase.context.Output = out
|
||||
err := TrustTagWrite(testcase.context, signedTags)
|
||||
if err != nil {
|
||||
assert.EqualError(t, err, testcase.expected)
|
||||
} else {
|
||||
assert.Equal(t, testcase.expected, out.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// With no trust data, the TrustTagWrite will print an empty table:
|
||||
// it's up to the caller to decide whether or not to print this versus an error
|
||||
func TestTrustTagContextEmptyWrite(t *testing.T) {
|
||||
|
||||
emptyCase := struct {
|
||||
context Context
|
||||
expected string
|
||||
}{
|
||||
Context{
|
||||
Format: NewTrustTagFormat(),
|
||||
},
|
||||
`SIGNED TAG DIGEST SIGNERS
|
||||
`,
|
||||
}
|
||||
|
||||
emptySignedTags := []SignedTagInfo{}
|
||||
out := bytes.NewBufferString("")
|
||||
emptyCase.context.Output = out
|
||||
err := TrustTagWrite(emptyCase.context, emptySignedTags)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, emptyCase.expected, out.String())
|
||||
}
|
||||
|
||||
func TestSignerInfoContextEmptyWrite(t *testing.T) {
|
||||
emptyCase := struct {
|
||||
context Context
|
||||
expected string
|
||||
}{
|
||||
Context{
|
||||
Format: NewSignerInfoFormat(),
|
||||
},
|
||||
`SIGNER KEYS
|
||||
`,
|
||||
}
|
||||
emptySignerInfo := []SignerInfo{}
|
||||
out := bytes.NewBufferString("")
|
||||
emptyCase.context.Output = out
|
||||
err := SignerInfoWrite(emptyCase.context, emptySignerInfo)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, emptyCase.expected, out.String())
|
||||
}
|
||||
|
||||
func TestSignerInfoContextWrite(t *testing.T) {
|
||||
cases := []struct {
|
||||
context Context
|
||||
expected string
|
||||
}{
|
||||
// Errors
|
||||
{
|
||||
Context{
|
||||
Format: "{{InvalidFunction}}",
|
||||
},
|
||||
`Template parsing error: template: :1: function "InvalidFunction" not defined
|
||||
`,
|
||||
},
|
||||
{
|
||||
Context{
|
||||
Format: "{{nil}}",
|
||||
},
|
||||
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
|
||||
`,
|
||||
},
|
||||
// Table Format
|
||||
{
|
||||
Context{
|
||||
Format: NewSignerInfoFormat(),
|
||||
Trunc: true,
|
||||
},
|
||||
`SIGNER KEYS
|
||||
alice key11, key12
|
||||
bob key21
|
||||
eve foobarbazqux, key31, key32
|
||||
`,
|
||||
},
|
||||
// No truncation
|
||||
{
|
||||
Context{
|
||||
Format: NewSignerInfoFormat(),
|
||||
},
|
||||
`SIGNER KEYS
|
||||
alice key11, key12
|
||||
bob key21
|
||||
eve foobarbazquxquux, key31, key32
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range cases {
|
||||
signerInfo := SignerInfoList{
|
||||
{Name: "alice", Keys: []string{"key11", "key12"}},
|
||||
{Name: "bob", Keys: []string{"key21"}},
|
||||
{Name: "eve", Keys: []string{"key31", "key32", "foobarbazquxquux"}},
|
||||
}
|
||||
out := bytes.NewBufferString("")
|
||||
testcase.context.Output = out
|
||||
err := SignerInfoWrite(testcase.context, signerInfo)
|
||||
if err != nil {
|
||||
assert.EqualError(t, err, testcase.expected)
|
||||
} else {
|
||||
assert.Equal(t, testcase.expected, out.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func runPush(dockerCli command.Cli, remote string) error {
|
||||
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "push")
|
||||
|
||||
if command.IsTrusted() {
|
||||
return trustedPush(ctx, dockerCli, repoInfo, ref, authConfig, requestPrivilege)
|
||||
return TrustedPush(ctx, dockerCli, repoInfo, ref, authConfig, requestPrivilege)
|
||||
}
|
||||
|
||||
responseBody, err := imagePushPrivileged(ctx, dockerCli, authConfig, ref, requestPrivilege)
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"sort"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
@@ -28,8 +27,8 @@ type target struct {
|
||||
size int64
|
||||
}
|
||||
|
||||
// trustedPush handles content trust pushing of an image
|
||||
func trustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error {
|
||||
// TrustedPush handles content trust pushing of an image
|
||||
func TrustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error {
|
||||
responseBody, err := imagePushPrivileged(ctx, cli, authConfig, ref, requestPrivilege)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -103,25 +102,25 @@ func PushTrustedReference(streams command.Streams, repoInfo *registry.Repository
|
||||
|
||||
fmt.Fprintln(streams.Out(), "Signing and pushing trust metadata")
|
||||
|
||||
repo, err := trust.GetNotaryRepository(streams, repoInfo, authConfig, "push", "pull")
|
||||
repo, err := trust.GetNotaryRepository(streams.In(), streams.Out(), command.UserAgent(), repoInfo, &authConfig, "push", "pull")
|
||||
if err != nil {
|
||||
fmt.Fprintf(streams.Out(), "Error establishing connection to notary repository: %s\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// get the latest repository metadata so we can figure out which roles to sign
|
||||
err = repo.Update(false)
|
||||
_, err = repo.ListTargets()
|
||||
|
||||
switch err.(type) {
|
||||
case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist:
|
||||
keys := repo.CryptoService.ListKeys(data.CanonicalRootRole)
|
||||
keys := repo.GetCryptoService().ListKeys(data.CanonicalRootRole)
|
||||
var rootKeyID string
|
||||
// always select the first root key
|
||||
if len(keys) > 0 {
|
||||
sort.Strings(keys)
|
||||
rootKeyID = keys[0]
|
||||
} else {
|
||||
rootPublicKey, err := repo.CryptoService.Create(data.CanonicalRootRole, "", data.ECDSAKey)
|
||||
rootPublicKey, err := repo.GetCryptoService().Create(data.CanonicalRootRole, "", data.ECDSAKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -136,7 +135,7 @@ func PushTrustedReference(streams command.Streams, repoInfo *registry.Repository
|
||||
err = repo.AddTarget(target, data.CanonicalTargetsRole)
|
||||
case nil:
|
||||
// already initialized and we have successfully downloaded the latest metadata
|
||||
err = addTargetToAllSignableRoles(repo, target)
|
||||
err = AddTargetToAllSignableRoles(repo, target)
|
||||
default:
|
||||
return trust.NotaryError(repoInfo.Name.Name(), err)
|
||||
}
|
||||
@@ -154,51 +153,16 @@ func PushTrustedReference(streams command.Streams, repoInfo *registry.Repository
|
||||
return nil
|
||||
}
|
||||
|
||||
// Attempt to add the image target to all the top level delegation roles we can
|
||||
// AddTargetToAllSignableRoles attempts to add the image target to all the top level delegation roles we can
|
||||
// (based on whether we have the signing key and whether the role's path allows
|
||||
// us to).
|
||||
// If there are no delegation roles, we add to the targets role.
|
||||
func addTargetToAllSignableRoles(repo *client.NotaryRepository, target *client.Target) error {
|
||||
var signableRoles []string
|
||||
|
||||
// translate the full key names, which includes the GUN, into just the key IDs
|
||||
allCanonicalKeyIDs := make(map[string]struct{})
|
||||
for fullKeyID := range repo.CryptoService.ListAllKeys() {
|
||||
allCanonicalKeyIDs[path.Base(fullKeyID)] = struct{}{}
|
||||
}
|
||||
|
||||
allDelegationRoles, err := repo.GetDelegationRoles()
|
||||
func AddTargetToAllSignableRoles(repo client.Repository, target *client.Target) error {
|
||||
signableRoles, err := trust.GetSignableRoles(repo, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if there are no delegation roles, then just try to sign it into the targets role
|
||||
if len(allDelegationRoles) == 0 {
|
||||
return repo.AddTarget(target, data.CanonicalTargetsRole)
|
||||
}
|
||||
|
||||
// there are delegation roles, find every delegation role we have a key for, and
|
||||
// attempt to sign into into all those roles.
|
||||
for _, delegationRole := range allDelegationRoles {
|
||||
// We do not support signing any delegation role that isn't a direct child of the targets role.
|
||||
// Also don't bother checking the keys if we can't add the target
|
||||
// to this role due to path restrictions
|
||||
if path.Dir(delegationRole.Name) != data.CanonicalTargetsRole || !delegationRole.CheckPaths(target.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, canonicalKeyID := range delegationRole.KeyIDs {
|
||||
if _, ok := allCanonicalKeyIDs[canonicalKeyID]; ok {
|
||||
signableRoles = append(signableRoles, delegationRole.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(signableRoles) == 0 {
|
||||
return errors.Errorf("no valid signing keys for delegation roles")
|
||||
}
|
||||
|
||||
return repo.AddTarget(target, signableRoles...)
|
||||
}
|
||||
|
||||
@@ -220,7 +184,7 @@ func imagePushPrivileged(ctx context.Context, cli command.Cli, authConfig types.
|
||||
func trustedPull(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error {
|
||||
var refs []target
|
||||
|
||||
notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull")
|
||||
notaryRepo, err := trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), repoInfo, &authConfig, "pull")
|
||||
if err != nil {
|
||||
fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err)
|
||||
return err
|
||||
@@ -335,7 +299,7 @@ func TrustedReference(ctx context.Context, cli command.Cli, ref reference.NamedT
|
||||
// Resolve the Auth config relevant for this server
|
||||
authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index)
|
||||
|
||||
notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull")
|
||||
notaryRepo, err := trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), repoInfo, &authConfig, "pull")
|
||||
if err != nil {
|
||||
fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err)
|
||||
return nil, err
|
||||
@@ -348,7 +312,7 @@ func TrustedReference(ctx context.Context, cli command.Cli, ref reference.NamedT
|
||||
// Only list tags in the top level targets role or the releases delegation role - ignore
|
||||
// all other delegation roles
|
||||
if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole {
|
||||
return nil, trust.NotaryError(repoInfo.Name.Name(), errors.Errorf("No trust data for %s", ref.Tag()))
|
||||
return nil, trust.NotaryError(repoInfo.Name.Name(), client.ErrNoSuchTarget(ref.Tag()))
|
||||
}
|
||||
r, err := convertTarget(t.Target)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/trust"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/notary/client"
|
||||
"github.com/docker/notary/passphrase"
|
||||
"github.com/docker/notary/trustpinning"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func unsetENV() {
|
||||
@@ -55,3 +60,14 @@ func TestNonOfficialTrustServer(t *testing.T) {
|
||||
t.Fatalf("Expected server to be %s, got %s", expectedStr, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddTargetToAllSignableRolesError(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("password"), trustpinning.TrustPinConfig{})
|
||||
target := client.Target{}
|
||||
err = AddTargetToAllSignableRoles(notaryRepo, &target)
|
||||
assert.EqualError(t, err, "client is offline")
|
||||
}
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
"vbom.ml/util/sortorder"
|
||||
)
|
||||
|
||||
type bySecretName []swarm.Secret
|
||||
|
||||
func (r bySecretName) Len() int { return len(r) }
|
||||
func (r bySecretName) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
||||
func (r bySecretName) Less(i, j int) bool {
|
||||
return sortorder.NaturalLess(r[i].Spec.Name, r[j].Spec.Name)
|
||||
}
|
||||
|
||||
type listOptions struct {
|
||||
quiet bool
|
||||
format string
|
||||
@@ -53,6 +65,9 @@ func runSecretList(dockerCli command.Cli, options listOptions) error {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(bySecretName(secrets))
|
||||
|
||||
secretCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewSecretFormat(format, options.quiet),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -48,18 +47,24 @@ func TestSecretListErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSecretList(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) {
|
||||
return []swarm.Secret{
|
||||
*Secret(SecretID("ID-foo"),
|
||||
SecretName("foo"),
|
||||
*Secret(SecretID("ID-1-foo"),
|
||||
SecretName("1-foo"),
|
||||
SecretVersion(swarm.Version{Index: 10}),
|
||||
SecretCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
SecretUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*Secret(SecretID("ID-bar"),
|
||||
SecretName("bar"),
|
||||
*Secret(SecretID("ID-10-foo"),
|
||||
SecretName("10-foo"),
|
||||
SecretVersion(swarm.Version{Index: 11}),
|
||||
SecretCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
SecretUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
SecretDriver("driver"),
|
||||
),
|
||||
*Secret(SecretID("ID-2-foo"),
|
||||
SecretName("2-foo"),
|
||||
SecretVersion(swarm.Version{Index: 11}),
|
||||
SecretCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
SecretUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
@@ -69,9 +74,8 @@ func TestSecretList(t *testing.T) {
|
||||
},
|
||||
})
|
||||
cmd := newSecretListCommand(cli)
|
||||
cmd.SetOutput(buf)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "secret-list.golden")
|
||||
golden.Assert(t, cli.OutBuffer().String(), "secret-list-sort.golden")
|
||||
}
|
||||
|
||||
func TestSecretListWithQuietOption(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
ID NAME DRIVER CREATED UPDATED
|
||||
ID-1-foo 1-foo 2 hours ago About an hour ago
|
||||
ID-2-foo 2-foo driver 2 hours ago About an hour ago
|
||||
ID-10-foo 10-foo driver 2 hours ago About an hour ago
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
foo
|
||||
bar label=label-bar
|
||||
foo
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
ID NAME DRIVER CREATED UPDATED
|
||||
ID-foo foo 2 hours ago About an hour ago
|
||||
ID-bar bar 2 hours ago About an hour ago
|
||||
ID-foo foo 2 hours ago About an hour ago
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
foo
|
||||
bar label=label-bar
|
||||
foo
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
ID-foo
|
||||
ID-bar
|
||||
ID-foo
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
ID NAME DRIVER CREATED UPDATED
|
||||
ID-foo foo 2 hours ago About an hour ago
|
||||
ID-bar bar driver 2 hours ago About an hour ago
|
||||
@@ -59,7 +59,7 @@ func trustedResolveDigest(ctx context.Context, cli command.Cli, ref reference.Na
|
||||
|
||||
authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index)
|
||||
|
||||
notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull")
|
||||
notaryRepo, err := trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), repoInfo, &authConfig, "pull")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error establishing connection to trust repository")
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/docker/cli/templates"
|
||||
"github.com/docker/docker/api/types"
|
||||
eventtypes "github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/pkg/jsonlog"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
@@ -104,14 +103,18 @@ func makeTemplate(format string) (*template.Template, error) {
|
||||
return tmpl, tmpl.Execute(ioutil.Discard, &eventtypes.Message{})
|
||||
}
|
||||
|
||||
// rfc3339NanoFixed is similar to time.RFC3339Nano, except it pads nanoseconds
|
||||
// zeros to maintain a fixed number of characters
|
||||
const rfc3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00"
|
||||
|
||||
// prettyPrintEvent prints all types of event information.
|
||||
// Each output includes the event type, actor id, name and action.
|
||||
// Actor attributes are printed at the end if the actor has any.
|
||||
func prettyPrintEvent(out io.Writer, event eventtypes.Message) error {
|
||||
if event.TimeNano != 0 {
|
||||
fmt.Fprintf(out, "%s ", time.Unix(0, event.TimeNano).Format(jsonlog.RFC3339NanoFixed))
|
||||
fmt.Fprintf(out, "%s ", time.Unix(0, event.TimeNano).Format(rfc3339NanoFixed))
|
||||
} else if event.Time != 0 {
|
||||
fmt.Fprintf(out, "%s ", time.Unix(event.Time, 0).Format(jsonlog.RFC3339NanoFixed))
|
||||
fmt.Fprintf(out, "%s ", time.Unix(event.Time, 0).Format(rfc3339NanoFixed))
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "%s %s %s", event.Type, event.Action, event.Actor.ID)
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
package trust
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/notary/client"
|
||||
"github.com/docker/notary/client/changelist"
|
||||
"github.com/docker/notary/cryptoservice"
|
||||
"github.com/docker/notary/passphrase"
|
||||
"github.com/docker/notary/storage"
|
||||
"github.com/docker/notary/trustmanager"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/docker/notary/tuf/signed"
|
||||
)
|
||||
|
||||
// Sample mock CLI interfaces
|
||||
|
||||
func getOfflineNotaryRepository(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client.Repository, error) {
|
||||
return OfflineNotaryRepository{}, nil
|
||||
}
|
||||
|
||||
// OfflineNotaryRepository is a mock Notary repository that is offline
|
||||
type OfflineNotaryRepository struct{}
|
||||
|
||||
func (o OfflineNotaryRepository) Initialize(rootKeyIDs []string, serverManagedRoles ...data.RoleName) error {
|
||||
return storage.ErrOffline{}
|
||||
}
|
||||
|
||||
func (o OfflineNotaryRepository) InitializeWithCertificate(rootKeyIDs []string, rootCerts []data.PublicKey, serverManagedRoles ...data.RoleName) error {
|
||||
return storage.ErrOffline{}
|
||||
}
|
||||
func (o OfflineNotaryRepository) Publish() error {
|
||||
return storage.ErrOffline{}
|
||||
}
|
||||
|
||||
func (o OfflineNotaryRepository) AddTarget(target *client.Target, roles ...data.RoleName) error {
|
||||
return nil
|
||||
}
|
||||
func (o OfflineNotaryRepository) RemoveTarget(targetName string, roles ...data.RoleName) error {
|
||||
return nil
|
||||
}
|
||||
func (o OfflineNotaryRepository) ListTargets(roles ...data.RoleName) ([]*client.TargetWithRole, error) {
|
||||
return nil, storage.ErrOffline{}
|
||||
}
|
||||
|
||||
func (o OfflineNotaryRepository) GetTargetByName(name string, roles ...data.RoleName) (*client.TargetWithRole, error) {
|
||||
return nil, storage.ErrOffline{}
|
||||
}
|
||||
|
||||
func (o OfflineNotaryRepository) GetAllTargetMetadataByName(name string) ([]client.TargetSignedStruct, error) {
|
||||
return nil, storage.ErrOffline{}
|
||||
}
|
||||
|
||||
func (o OfflineNotaryRepository) GetChangelist() (changelist.Changelist, error) {
|
||||
return changelist.NewMemChangelist(), nil
|
||||
}
|
||||
|
||||
func (o OfflineNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) {
|
||||
return nil, storage.ErrOffline{}
|
||||
}
|
||||
|
||||
func (o OfflineNotaryRepository) GetDelegationRoles() ([]data.Role, error) {
|
||||
return nil, storage.ErrOffline{}
|
||||
}
|
||||
|
||||
func (o OfflineNotaryRepository) AddDelegation(name data.RoleName, delegationKeys []data.PublicKey, paths []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o OfflineNotaryRepository) AddDelegationRoleAndKeys(name data.RoleName, delegationKeys []data.PublicKey) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o OfflineNotaryRepository) AddDelegationPaths(name data.RoleName, paths []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o OfflineNotaryRepository) RemoveDelegationKeysAndPaths(name data.RoleName, keyIDs, paths []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o OfflineNotaryRepository) RemoveDelegationRole(name data.RoleName) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o OfflineNotaryRepository) RemoveDelegationPaths(name data.RoleName, paths []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o OfflineNotaryRepository) RemoveDelegationKeys(name data.RoleName, keyIDs []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o OfflineNotaryRepository) ClearDelegationPaths(name data.RoleName) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o OfflineNotaryRepository) Witness(roles ...data.RoleName) ([]data.RoleName, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (o OfflineNotaryRepository) RotateKey(role data.RoleName, serverManagesKey bool, keyList []string) error {
|
||||
return storage.ErrOffline{}
|
||||
}
|
||||
|
||||
func (o OfflineNotaryRepository) GetCryptoService() signed.CryptoService {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o OfflineNotaryRepository) SetLegacyVersions(version int) {}
|
||||
|
||||
func (o OfflineNotaryRepository) GetGUN() data.GUN {
|
||||
return data.GUN("gun")
|
||||
}
|
||||
|
||||
func getUninitializedNotaryRepository(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client.Repository, error) {
|
||||
return UninitializedNotaryRepository{}, nil
|
||||
}
|
||||
|
||||
// UninitializedNotaryRepository is a mock Notary repository that is uninintialized
|
||||
// it builds on top of the OfflineNotaryRepository, instead returning ErrRepositoryNotExist
|
||||
// for any online operation
|
||||
type UninitializedNotaryRepository struct {
|
||||
OfflineNotaryRepository
|
||||
}
|
||||
|
||||
func (u UninitializedNotaryRepository) Initialize(rootKeyIDs []string, serverManagedRoles ...data.RoleName) error {
|
||||
return client.ErrRepositoryNotExist{}
|
||||
}
|
||||
|
||||
func (u UninitializedNotaryRepository) InitializeWithCertificate(rootKeyIDs []string, rootCerts []data.PublicKey, serverManagedRoles ...data.RoleName) error {
|
||||
return client.ErrRepositoryNotExist{}
|
||||
}
|
||||
func (u UninitializedNotaryRepository) Publish() error {
|
||||
return client.ErrRepositoryNotExist{}
|
||||
}
|
||||
|
||||
func (u UninitializedNotaryRepository) ListTargets(roles ...data.RoleName) ([]*client.TargetWithRole, error) {
|
||||
return nil, client.ErrRepositoryNotExist{}
|
||||
}
|
||||
|
||||
func (u UninitializedNotaryRepository) GetTargetByName(name string, roles ...data.RoleName) (*client.TargetWithRole, error) {
|
||||
return nil, client.ErrRepositoryNotExist{}
|
||||
}
|
||||
|
||||
func (u UninitializedNotaryRepository) GetAllTargetMetadataByName(name string) ([]client.TargetSignedStruct, error) {
|
||||
return nil, client.ErrRepositoryNotExist{}
|
||||
}
|
||||
|
||||
func (u UninitializedNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) {
|
||||
return nil, client.ErrRepositoryNotExist{}
|
||||
}
|
||||
|
||||
func (u UninitializedNotaryRepository) GetDelegationRoles() ([]data.Role, error) {
|
||||
return nil, client.ErrRepositoryNotExist{}
|
||||
}
|
||||
|
||||
func (u UninitializedNotaryRepository) RotateKey(role data.RoleName, serverManagesKey bool, keyList []string) error {
|
||||
return client.ErrRepositoryNotExist{}
|
||||
}
|
||||
|
||||
func getEmptyTargetsNotaryRepository(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client.Repository, error) {
|
||||
return EmptyTargetsNotaryRepository{}, nil
|
||||
}
|
||||
|
||||
// EmptyTargetsNotaryRepository is a mock Notary repository that is initialized
|
||||
// but does not have any signed targets
|
||||
type EmptyTargetsNotaryRepository struct {
|
||||
OfflineNotaryRepository
|
||||
}
|
||||
|
||||
func (e EmptyTargetsNotaryRepository) Initialize(rootKeyIDs []string, serverManagedRoles ...data.RoleName) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e EmptyTargetsNotaryRepository) InitializeWithCertificate(rootKeyIDs []string, rootCerts []data.PublicKey, serverManagedRoles ...data.RoleName) error {
|
||||
return nil
|
||||
}
|
||||
func (e EmptyTargetsNotaryRepository) Publish() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e EmptyTargetsNotaryRepository) ListTargets(roles ...data.RoleName) ([]*client.TargetWithRole, error) {
|
||||
return []*client.TargetWithRole{}, nil
|
||||
}
|
||||
|
||||
func (e EmptyTargetsNotaryRepository) GetTargetByName(name string, roles ...data.RoleName) (*client.TargetWithRole, error) {
|
||||
return nil, client.ErrNoSuchTarget(name)
|
||||
}
|
||||
|
||||
func (e EmptyTargetsNotaryRepository) GetAllTargetMetadataByName(name string) ([]client.TargetSignedStruct, error) {
|
||||
return nil, client.ErrNoSuchTarget(name)
|
||||
}
|
||||
|
||||
func (e EmptyTargetsNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) {
|
||||
return []client.RoleWithSignatures{}, nil
|
||||
}
|
||||
|
||||
func (e EmptyTargetsNotaryRepository) GetDelegationRoles() ([]data.Role, error) {
|
||||
return []data.Role{}, nil
|
||||
}
|
||||
|
||||
func (e EmptyTargetsNotaryRepository) RotateKey(role data.RoleName, serverManagesKey bool, keyList []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLoadedNotaryRepository(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client.Repository, error) {
|
||||
return LoadedNotaryRepository{}, nil
|
||||
}
|
||||
|
||||
// LoadedNotaryRepository is a mock Notary repository that is loaded with targets, delegations, and keys
|
||||
type LoadedNotaryRepository struct {
|
||||
EmptyTargetsNotaryRepository
|
||||
statefulCryptoService signed.CryptoService
|
||||
}
|
||||
|
||||
// LoadedNotaryRepository has three delegations:
|
||||
// - targets/releases: includes keys A and B
|
||||
// - targets/alice: includes key A
|
||||
// - targets/bob: includes key B
|
||||
var loadedReleasesRole = data.DelegationRole{
|
||||
BaseRole: data.BaseRole{
|
||||
Name: "targets/releases",
|
||||
Keys: map[string]data.PublicKey{"A": nil, "B": nil},
|
||||
Threshold: 1,
|
||||
},
|
||||
}
|
||||
var loadedAliceRole = data.DelegationRole{
|
||||
BaseRole: data.BaseRole{
|
||||
Name: "targets/alice",
|
||||
Keys: map[string]data.PublicKey{"A": nil},
|
||||
Threshold: 1,
|
||||
},
|
||||
}
|
||||
var loadedBobRole = data.DelegationRole{
|
||||
BaseRole: data.BaseRole{
|
||||
Name: "targets/bob",
|
||||
Keys: map[string]data.PublicKey{"B": nil},
|
||||
Threshold: 1,
|
||||
},
|
||||
}
|
||||
var loadedDelegationRoles = []data.Role{
|
||||
{
|
||||
Name: loadedReleasesRole.Name,
|
||||
RootRole: data.RootRole{
|
||||
KeyIDs: []string{"A", "B"},
|
||||
Threshold: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: loadedAliceRole.Name,
|
||||
RootRole: data.RootRole{
|
||||
KeyIDs: []string{"A"},
|
||||
Threshold: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: loadedBobRole.Name,
|
||||
RootRole: data.RootRole{
|
||||
KeyIDs: []string{"B"},
|
||||
Threshold: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
var loadedTargetsRole = data.DelegationRole{
|
||||
BaseRole: data.BaseRole{
|
||||
Name: data.CanonicalTargetsRole,
|
||||
Keys: map[string]data.PublicKey{"C": nil},
|
||||
Threshold: 1,
|
||||
},
|
||||
}
|
||||
|
||||
// LoadedNotaryRepository has three targets:
|
||||
// - red: signed by targets/releases, targets/alice, targets/bob
|
||||
// - blue: signed by targets/releases, targets/alice
|
||||
// - green: signed by targets/releases
|
||||
var loadedRedTarget = client.Target{
|
||||
Name: "red",
|
||||
Hashes: data.Hashes{"sha256": []byte("red-digest")},
|
||||
}
|
||||
var loadedBlueTarget = client.Target{
|
||||
Name: "blue",
|
||||
Hashes: data.Hashes{"sha256": []byte("blue-digest")},
|
||||
}
|
||||
var loadedGreenTarget = client.Target{
|
||||
Name: "green",
|
||||
Hashes: data.Hashes{"sha256": []byte("green-digest")},
|
||||
}
|
||||
var loadedTargets = []client.TargetSignedStruct{
|
||||
// red is signed by all three delegations
|
||||
{Target: loadedRedTarget, Role: loadedReleasesRole},
|
||||
{Target: loadedRedTarget, Role: loadedAliceRole},
|
||||
{Target: loadedRedTarget, Role: loadedBobRole},
|
||||
|
||||
// blue is signed by targets/releases, targets/alice
|
||||
{Target: loadedBlueTarget, Role: loadedReleasesRole},
|
||||
{Target: loadedBlueTarget, Role: loadedAliceRole},
|
||||
|
||||
// green is signed by targets/releases
|
||||
{Target: loadedGreenTarget, Role: loadedReleasesRole},
|
||||
}
|
||||
|
||||
func (l LoadedNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) {
|
||||
rootRole := data.Role{
|
||||
RootRole: data.RootRole{
|
||||
KeyIDs: []string{"rootID"},
|
||||
Threshold: 1,
|
||||
},
|
||||
Name: data.CanonicalRootRole,
|
||||
}
|
||||
|
||||
targetsRole := data.Role{
|
||||
RootRole: data.RootRole{
|
||||
KeyIDs: []string{"targetsID"},
|
||||
Threshold: 1,
|
||||
},
|
||||
Name: data.CanonicalTargetsRole,
|
||||
}
|
||||
|
||||
return []client.RoleWithSignatures{{Role: rootRole}, {Role: targetsRole}}, nil
|
||||
}
|
||||
|
||||
func (l LoadedNotaryRepository) ListTargets(roles ...data.RoleName) ([]*client.TargetWithRole, error) {
|
||||
filteredTargets := []*client.TargetWithRole{}
|
||||
for _, tgt := range loadedTargets {
|
||||
if len(roles) == 0 || (len(roles) > 0 && roles[0] == tgt.Role.Name) {
|
||||
filteredTargets = append(filteredTargets, &client.TargetWithRole{Target: tgt.Target, Role: tgt.Role.Name})
|
||||
}
|
||||
}
|
||||
return filteredTargets, nil
|
||||
}
|
||||
|
||||
func (l LoadedNotaryRepository) GetTargetByName(name string, roles ...data.RoleName) (*client.TargetWithRole, error) {
|
||||
for _, tgt := range loadedTargets {
|
||||
if name == tgt.Target.Name {
|
||||
if len(roles) == 0 || (len(roles) > 0 && roles[0] == tgt.Role.Name) {
|
||||
return &client.TargetWithRole{Target: tgt.Target, Role: tgt.Role.Name}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, client.ErrNoSuchTarget(name)
|
||||
}
|
||||
|
||||
func (l LoadedNotaryRepository) GetAllTargetMetadataByName(name string) ([]client.TargetSignedStruct, error) {
|
||||
if name == "" {
|
||||
return loadedTargets, nil
|
||||
}
|
||||
filteredTargets := []client.TargetSignedStruct{}
|
||||
for _, tgt := range loadedTargets {
|
||||
if name == tgt.Target.Name {
|
||||
filteredTargets = append(filteredTargets, tgt)
|
||||
}
|
||||
}
|
||||
if len(filteredTargets) == 0 {
|
||||
return nil, client.ErrNoSuchTarget(name)
|
||||
}
|
||||
return filteredTargets, nil
|
||||
}
|
||||
|
||||
func (l LoadedNotaryRepository) GetGUN() data.GUN {
|
||||
return data.GUN("signed-repo")
|
||||
}
|
||||
|
||||
func (l LoadedNotaryRepository) GetDelegationRoles() ([]data.Role, error) {
|
||||
return loadedDelegationRoles, nil
|
||||
}
|
||||
|
||||
func (l LoadedNotaryRepository) GetCryptoService() signed.CryptoService {
|
||||
if l.statefulCryptoService == nil {
|
||||
// give it an in-memory cryptoservice with a root key and targets key
|
||||
l.statefulCryptoService = cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(passphrase.ConstantRetriever("password")))
|
||||
l.statefulCryptoService.AddKey(data.CanonicalRootRole, l.GetGUN(), nil)
|
||||
l.statefulCryptoService.AddKey(data.CanonicalTargetsRole, l.GetGUN(), nil)
|
||||
}
|
||||
return l.statefulCryptoService
|
||||
}
|
||||
|
||||
func getLoadedWithNoSignersNotaryRepository(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client.Repository, error) {
|
||||
return LoadedWithNoSignersNotaryRepository{}, nil
|
||||
}
|
||||
|
||||
// LoadedWithNoSignersNotaryRepository is a mock Notary repository that is loaded with targets but no delegations
|
||||
// it only contains the green target
|
||||
type LoadedWithNoSignersNotaryRepository struct {
|
||||
LoadedNotaryRepository
|
||||
}
|
||||
|
||||
func (l LoadedWithNoSignersNotaryRepository) ListTargets(roles ...data.RoleName) ([]*client.TargetWithRole, error) {
|
||||
filteredTargets := []*client.TargetWithRole{}
|
||||
for _, tgt := range loadedTargets {
|
||||
if len(roles) == 0 || (len(roles) > 0 && roles[0] == tgt.Role.Name) {
|
||||
filteredTargets = append(filteredTargets, &client.TargetWithRole{Target: tgt.Target, Role: tgt.Role.Name})
|
||||
}
|
||||
}
|
||||
return filteredTargets, nil
|
||||
}
|
||||
|
||||
func (l LoadedWithNoSignersNotaryRepository) GetTargetByName(name string, roles ...data.RoleName) (*client.TargetWithRole, error) {
|
||||
if name == "" || name == loadedGreenTarget.Name {
|
||||
return &client.TargetWithRole{Target: loadedGreenTarget, Role: data.CanonicalTargetsRole}, nil
|
||||
}
|
||||
return nil, client.ErrNoSuchTarget(name)
|
||||
}
|
||||
|
||||
func (l LoadedWithNoSignersNotaryRepository) GetAllTargetMetadataByName(name string) ([]client.TargetSignedStruct, error) {
|
||||
if name == "" || name == loadedGreenTarget.Name {
|
||||
return []client.TargetSignedStruct{{Target: loadedGreenTarget, Role: loadedTargetsRole}}, nil
|
||||
}
|
||||
return nil, client.ErrNoSuchTarget(name)
|
||||
}
|
||||
|
||||
func (l LoadedWithNoSignersNotaryRepository) GetDelegationRoles() ([]data.Role, error) {
|
||||
return []data.Role{}, nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package trust
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewTrustCommand returns a cobra command for `trust` subcommands
|
||||
func NewTrustCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "trust",
|
||||
Short: "Manage trust on Docker images (experimental)",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newViewCommand(dockerCli),
|
||||
newRevokeCommand(dockerCli),
|
||||
newSignCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package trust
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/notary/client"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
)
|
||||
|
||||
const releasedRoleName = "Repo Admin"
|
||||
|
||||
// check 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
|
||||
func notaryRoleToSigner(tufRole data.RoleName) string {
|
||||
// don't show a signer for "targets" or "targets/releases"
|
||||
if isReleasedTarget(data.RoleName(tufRole.String())) {
|
||||
return releasedRoleName
|
||||
}
|
||||
return strings.TrimPrefix(tufRole.String(), "targets/")
|
||||
}
|
||||
|
||||
func clearChangeList(notaryRepo client.Repository) error {
|
||||
cl, err := notaryRepo.GetChangelist()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cl.Clear("")
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package trust
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/notary/client"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type revokeOptions struct {
|
||||
forceYes bool
|
||||
}
|
||||
|
||||
func newRevokeCommand(dockerCli command.Cli) *cobra.Command {
|
||||
options := revokeOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "revoke [OPTIONS] IMAGE[:TAG]",
|
||||
Short: "Remove trust for an image",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return revokeTrust(dockerCli, args[0], options)
|
||||
},
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&options.forceYes, "yes", "y", false, "Do not prompt for confirmation")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func revokeTrust(cli command.Cli, remote string, options revokeOptions) error {
|
||||
ctx := context.Background()
|
||||
authResolver := func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
|
||||
return command.ResolveAuthConfig(ctx, cli, index)
|
||||
}
|
||||
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, authResolver, remote)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tag := imgRefAndAuth.Tag()
|
||||
if imgRefAndAuth.Tag() == "" && imgRefAndAuth.Digest() != "" {
|
||||
return fmt.Errorf("cannot use a digest reference for IMAGE:TAG")
|
||||
}
|
||||
if imgRefAndAuth.Tag() == "" && !options.forceYes {
|
||||
deleteRemote := command.PromptForConfirmation(os.Stdin, cli.Out(), fmt.Sprintf("Please confirm you would like to delete all signature data for %s?", remote))
|
||||
if !deleteRemote {
|
||||
fmt.Fprintf(cli.Out(), "\nAborting action.\n")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
notaryRepo, err := cli.NotaryClient(*imgRefAndAuth, trust.ActionsPushAndPull)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = clearChangeList(notaryRepo); err != nil {
|
||||
return err
|
||||
}
|
||||
defer clearChangeList(notaryRepo)
|
||||
if err := revokeSignature(notaryRepo, tag); err != nil {
|
||||
return errors.Wrapf(err, "could not remove signature for %s", remote)
|
||||
}
|
||||
fmt.Fprintf(cli.Out(), "Successfully deleted signature for %s\n", remote)
|
||||
return nil
|
||||
}
|
||||
|
||||
func revokeSignature(notaryRepo client.Repository, tag string) error {
|
||||
if tag != "" {
|
||||
// Revoke signature for the specified tag
|
||||
if err := revokeSingleSig(notaryRepo, tag); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// revoke all signatures for the image, as no tag was given
|
||||
if err := revokeAllSigs(notaryRepo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Publish change
|
||||
return notaryRepo.Publish()
|
||||
}
|
||||
|
||||
func revokeSingleSig(notaryRepo client.Repository, tag string) error {
|
||||
releasedTargetWithRole, err := notaryRepo.GetTargetByName(tag, trust.ReleasesRole, data.CanonicalTargetsRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
releasedTarget := releasedTargetWithRole.Target
|
||||
return getSignableRolesForTargetAndRemove(releasedTarget, notaryRepo)
|
||||
}
|
||||
|
||||
func revokeAllSigs(notaryRepo client.Repository) error {
|
||||
releasedTargetWithRoleList, err := notaryRepo.ListTargets(trust.ReleasesRole, data.CanonicalTargetsRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(releasedTargetWithRoleList) == 0 {
|
||||
return fmt.Errorf("no signed tags to remove")
|
||||
}
|
||||
|
||||
// we need all the roles that signed each released target so we can remove from all roles.
|
||||
for _, releasedTargetWithRole := range releasedTargetWithRoleList {
|
||||
// remove from all roles
|
||||
if err := getSignableRolesForTargetAndRemove(releasedTargetWithRole.Target, notaryRepo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// get all the roles that signed the target and removes it from all roles.
|
||||
func getSignableRolesForTargetAndRemove(releasedTarget client.Target, notaryRepo client.Repository) error {
|
||||
signableRoles, err := trust.GetSignableRoles(notaryRepo, &releasedTarget)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// remove from all roles
|
||||
return notaryRepo.RemoveTarget(releasedTarget.Name, signableRoles...)
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package trust
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/notary/client"
|
||||
"github.com/docker/notary/passphrase"
|
||||
"github.com/docker/notary/trustpinning"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTrustRevokeCommandErrors(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{"remote1", "remote2"},
|
||||
expectedError: "requires exactly 1 argument",
|
||||
},
|
||||
{
|
||||
name: "sha-reference",
|
||||
args: []string{"870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd"},
|
||||
expectedError: "invalid repository name",
|
||||
},
|
||||
{
|
||||
name: "invalid-img-reference",
|
||||
args: []string{"ALPINE"},
|
||||
expectedError: "invalid reference format",
|
||||
},
|
||||
{
|
||||
name: "digest-reference",
|
||||
args: []string{"ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2"},
|
||||
expectedError: "cannot use a digest reference for IMAGE:TAG",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := newRevokeCommand(
|
||||
test.NewFakeCli(&fakeClient{}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustRevokeCommandOfflineErrors(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getOfflineNotaryRepository)
|
||||
cmd := newRevokeCommand(cli)
|
||||
cmd.SetArgs([]string{"reg-name.io/image"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
assert.Contains(t, cli.OutBuffer().String(), "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.")
|
||||
|
||||
cli = test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getOfflineNotaryRepository)
|
||||
cmd = newRevokeCommand(cli)
|
||||
cmd.SetArgs([]string{"reg-name.io/image", "-y"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
|
||||
cli = test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getOfflineNotaryRepository)
|
||||
cmd = newRevokeCommand(cli)
|
||||
cmd.SetArgs([]string{"reg-name.io/image:tag"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image:tag: client is offline")
|
||||
}
|
||||
|
||||
func TestTrustRevokeCommandUninitializedErrors(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getUninitializedNotaryRepository)
|
||||
cmd := newRevokeCommand(cli)
|
||||
cmd.SetArgs([]string{"reg-name.io/image"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
assert.Contains(t, cli.OutBuffer().String(), "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.")
|
||||
|
||||
cli = test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getUninitializedNotaryRepository)
|
||||
cmd = newRevokeCommand(cli)
|
||||
cmd.SetArgs([]string{"reg-name.io/image", "-y"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image: does not have trust data for")
|
||||
|
||||
cli = test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getUninitializedNotaryRepository)
|
||||
cmd = newRevokeCommand(cli)
|
||||
cmd.SetArgs([]string{"reg-name.io/image:tag"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image:tag: does not have trust data for")
|
||||
}
|
||||
|
||||
func TestTrustRevokeCommandEmptyNotaryRepo(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
|
||||
cmd := newRevokeCommand(cli)
|
||||
cmd.SetArgs([]string{"reg-name.io/image"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
assert.Contains(t, cli.OutBuffer().String(), "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.")
|
||||
|
||||
cli = test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
|
||||
cmd = newRevokeCommand(cli)
|
||||
cmd.SetArgs([]string{"reg-name.io/image", "-y"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image: no signed tags to remove")
|
||||
|
||||
cli = test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
|
||||
cmd = newRevokeCommand(cli)
|
||||
cmd.SetArgs([]string{"reg-name.io/image:tag"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image:tag: No valid trust data for tag")
|
||||
}
|
||||
|
||||
func TestNewRevokeTrustAllSigConfirmation(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
|
||||
cmd := newRevokeCommand(cli)
|
||||
cmd.SetArgs([]string{"alpine"})
|
||||
assert.NoError(t, cmd.Execute())
|
||||
|
||||
assert.Contains(t, cli.OutBuffer().String(), "Please confirm you would like to delete all signature data for alpine? [y/N] \nAborting action.")
|
||||
}
|
||||
|
||||
func TestGetSignableRolesForTargetAndRemoveError(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("password"), trustpinning.TrustPinConfig{})
|
||||
target := client.Target{}
|
||||
err = getSignableRolesForTargetAndRemove(target, notaryRepo)
|
||||
assert.EqualError(t, err, "client is offline")
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package trust
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"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/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/notary/client"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newSignCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "sign [OPTIONS] IMAGE:TAG",
|
||||
Short: "Sign an image",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return signImage(dockerCli, args[0])
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func signImage(cli command.Cli, imageName string) error {
|
||||
ctx := context.Background()
|
||||
authResolver := func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
|
||||
return command.ResolveAuthConfig(ctx, cli, index)
|
||||
}
|
||||
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, authResolver, imageName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tag := imgRefAndAuth.Tag()
|
||||
if tag == "" {
|
||||
if imgRefAndAuth.Digest() != "" {
|
||||
return fmt.Errorf("cannot use a digest reference for IMAGE:TAG")
|
||||
}
|
||||
return fmt.Errorf("No tag specified for %s", imageName)
|
||||
}
|
||||
|
||||
notaryRepo, err := cli.NotaryClient(*imgRefAndAuth, trust.ActionsPushAndPull)
|
||||
if err != nil {
|
||||
return trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
|
||||
}
|
||||
if err = clearChangeList(notaryRepo); err != nil {
|
||||
return err
|
||||
}
|
||||
defer clearChangeList(notaryRepo)
|
||||
|
||||
// get the latest repository metadata so we can figure out which roles to sign
|
||||
if _, err = notaryRepo.ListTargets(); err != nil {
|
||||
switch err.(type) {
|
||||
case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist:
|
||||
// before initializing a new repo, check that the image exists locally:
|
||||
if err := checkLocalImageExistence(ctx, cli, imageName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userRole := data.RoleName(path.Join(data.CanonicalTargetsRole.String(), imgRefAndAuth.AuthConfig().Username))
|
||||
if err := initNotaryRepoWithSigners(notaryRepo, userRole); err != nil {
|
||||
return trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(cli.Out(), "Created signer: %s\n", imgRefAndAuth.AuthConfig().Username)
|
||||
fmt.Fprintf(cli.Out(), "Finished initializing signed repository for %s\n", imageName)
|
||||
default:
|
||||
return trust.NotaryError(imgRefAndAuth.RepoInfo().Name.Name(), err)
|
||||
}
|
||||
}
|
||||
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(cli, imgRefAndAuth.RepoInfo().Index, "push")
|
||||
target, err := createTarget(notaryRepo, tag)
|
||||
if err != nil {
|
||||
switch err := err.(type) {
|
||||
case client.ErrNoSuchTarget, client.ErrRepositoryNotExist:
|
||||
// Fail fast if the image doesn't exist locally
|
||||
if err := checkLocalImageExistence(ctx, cli, imageName); err != nil {
|
||||
return err
|
||||
}
|
||||
return image.TrustedPush(ctx, cli, imgRefAndAuth.RepoInfo(), imgRefAndAuth.Reference(), *imgRefAndAuth.AuthConfig(), requestPrivilege)
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(cli.Out(), "Signing and pushing trust metadata for %s\n", imageName)
|
||||
existingSigInfo, err := getExistingSignatureInfoForReleasedTag(notaryRepo, tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = image.AddTargetToAllSignableRoles(notaryRepo, &target)
|
||||
if err == nil {
|
||||
prettyPrintExistingSignatureInfo(cli, existingSigInfo)
|
||||
err = notaryRepo.Publish()
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to sign %q:%s", imgRefAndAuth.RepoInfo().Name.Name(), tag)
|
||||
}
|
||||
fmt.Fprintf(cli.Out(), "Successfully signed %q:%s\n", imgRefAndAuth.RepoInfo().Name.Name(), tag)
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkLocalImageExistence(ctx context.Context, cli command.Cli, imageName string) error {
|
||||
_, _, err := cli.Client().ImageInspectWithRaw(ctx, imageName)
|
||||
return err
|
||||
}
|
||||
|
||||
func createTarget(notaryRepo client.Repository, tag string) (client.Target, error) {
|
||||
target := &client.Target{}
|
||||
var err error
|
||||
if tag == "" {
|
||||
return *target, fmt.Errorf("No tag specified")
|
||||
}
|
||||
target.Name = tag
|
||||
target.Hashes, target.Length, err = getSignedManifestHashAndSize(notaryRepo, tag)
|
||||
return *target, err
|
||||
}
|
||||
|
||||
func getSignedManifestHashAndSize(notaryRepo client.Repository, tag string) (data.Hashes, int64, error) {
|
||||
targets, err := notaryRepo.GetAllTargetMetadataByName(tag)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return getReleasedTargetHashAndSize(targets, tag)
|
||||
}
|
||||
|
||||
func getReleasedTargetHashAndSize(targets []client.TargetSignedStruct, tag string) (data.Hashes, int64, error) {
|
||||
for _, tgt := range targets {
|
||||
if isReleasedTarget(tgt.Role.Name) {
|
||||
return tgt.Target.Hashes, tgt.Target.Length, nil
|
||||
}
|
||||
}
|
||||
return nil, 0, client.ErrNoSuchTarget(tag)
|
||||
}
|
||||
|
||||
func getExistingSignatureInfoForReleasedTag(notaryRepo client.Repository, tag string) (trustTagRow, error) {
|
||||
targets, err := notaryRepo.GetAllTargetMetadataByName(tag)
|
||||
if err != nil {
|
||||
return trustTagRow{}, err
|
||||
}
|
||||
releasedTargetInfoList := matchReleasedSignatures(targets)
|
||||
if len(releasedTargetInfoList) == 0 {
|
||||
return trustTagRow{}, nil
|
||||
}
|
||||
return releasedTargetInfoList[0], nil
|
||||
}
|
||||
|
||||
func prettyPrintExistingSignatureInfo(cli command.Cli, existingSigInfo trustTagRow) {
|
||||
sort.Strings(existingSigInfo.Signers)
|
||||
joinedSigners := strings.Join(existingSigInfo.Signers, ", ")
|
||||
fmt.Fprintf(cli.Out(), "Existing signatures for tag %s digest %s from:\n%s\n", existingSigInfo.TagName, existingSigInfo.HashHex, joinedSigners)
|
||||
}
|
||||
|
||||
func initNotaryRepoWithSigners(notaryRepo client.Repository, newSigner data.RoleName) error {
|
||||
rootKey, err := getOrGenerateNotaryKey(notaryRepo, data.CanonicalRootRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rootKeyID := rootKey.ID()
|
||||
|
||||
// Initialize the notary repository with a remotely managed snapshot key
|
||||
if err := notaryRepo.Initialize([]string{rootKeyID}, data.CanonicalSnapshotRole); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signerKey, err := getOrGenerateNotaryKey(notaryRepo, newSigner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addStagedSigner(notaryRepo, newSigner, []data.PublicKey{signerKey})
|
||||
|
||||
return notaryRepo.Publish()
|
||||
}
|
||||
|
||||
// generates an ECDSA key without a GUN for the specified role
|
||||
func getOrGenerateNotaryKey(notaryRepo client.Repository, role data.RoleName) (data.PublicKey, error) {
|
||||
// use the signer name in the PEM headers if this is a delegation key
|
||||
if data.IsDelegation(role) {
|
||||
role = data.RoleName(notaryRoleToSigner(role))
|
||||
}
|
||||
keys := notaryRepo.GetCryptoService().ListKeys(role)
|
||||
var err error
|
||||
var key data.PublicKey
|
||||
// always select the first key by ID
|
||||
if len(keys) > 0 {
|
||||
sort.Strings(keys)
|
||||
keyID := keys[0]
|
||||
privKey, _, err := notaryRepo.GetCryptoService().GetPrivateKey(keyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key = data.PublicKeyFromPrivate(privKey)
|
||||
} else {
|
||||
key, err = notaryRepo.GetCryptoService().Create(role, "", data.ECDSAKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// create targets/<username>
|
||||
notaryRepo.AddDelegationRoleAndKeys(newSigner, signerKeys)
|
||||
notaryRepo.AddDelegationPaths(newSigner, []string{""})
|
||||
|
||||
// create targets/releases
|
||||
notaryRepo.AddDelegationRoleAndKeys(trust.ReleasesRole, signerKeys)
|
||||
notaryRepo.AddDelegationPaths(trust.ReleasesRole, []string{""})
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package trust
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/notary"
|
||||
"github.com/docker/notary/client"
|
||||
"github.com/docker/notary/client/changelist"
|
||||
"github.com/docker/notary/passphrase"
|
||||
"github.com/docker/notary/trustpinning"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const passwd = "password"
|
||||
|
||||
func TestTrustSignCommandErrors(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{"image", "tag"},
|
||||
expectedError: "requires exactly 1 argument",
|
||||
},
|
||||
{
|
||||
name: "sha-reference",
|
||||
args: []string{"870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd"},
|
||||
expectedError: "invalid repository name",
|
||||
},
|
||||
{
|
||||
name: "invalid-img-reference",
|
||||
args: []string{"ALPINE:latest"},
|
||||
expectedError: "invalid reference format",
|
||||
},
|
||||
{
|
||||
name: "no-tag",
|
||||
args: []string{"reg/img"},
|
||||
expectedError: "No tag specified for reg/img",
|
||||
},
|
||||
{
|
||||
name: "digest-reference",
|
||||
args: []string{"ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2"},
|
||||
expectedError: "cannot use a digest reference for IMAGE:TAG",
|
||||
},
|
||||
}
|
||||
// change to a tmpdir
|
||||
tmpDir, err := ioutil.TempDir("", "docker-sign-test-")
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
config.SetDir(tmpDir)
|
||||
for _, tc := range testCases {
|
||||
cmd := newSignCommand(
|
||||
test.NewFakeCli(&fakeClient{}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustSignCommandOfflineErrors(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getOfflineNotaryRepository)
|
||||
cmd := newSignCommand(cli)
|
||||
cmd.SetArgs([]string{"reg-name.io/image:tag"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
assert.Error(t, cmd.Execute())
|
||||
testutil.ErrorContains(t, cmd.Execute(), "client is offline")
|
||||
}
|
||||
|
||||
func TestGetOrGenerateNotaryKey(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)
|
||||
|
||||
// repo is empty, try making a root key
|
||||
rootKeyA, err := getOrGenerateNotaryKey(notaryRepo, data.CanonicalRootRole)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, rootKeyA)
|
||||
|
||||
// we should only have one newly generated key
|
||||
allKeys := notaryRepo.GetCryptoService().ListAllKeys()
|
||||
assert.Len(t, allKeys, 1)
|
||||
assert.NotNil(t, notaryRepo.GetCryptoService().GetKey(rootKeyA.ID()))
|
||||
|
||||
// this time we should get back the same key if we ask for another root key
|
||||
rootKeyB, err := getOrGenerateNotaryKey(notaryRepo, data.CanonicalRootRole)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, rootKeyB)
|
||||
|
||||
// we should only have one newly generated key
|
||||
allKeys = notaryRepo.GetCryptoService().ListAllKeys()
|
||||
assert.Len(t, allKeys, 1)
|
||||
assert.NotNil(t, notaryRepo.GetCryptoService().GetKey(rootKeyB.ID()))
|
||||
|
||||
// The key we retrieved should be identical to the one we generated
|
||||
assert.Equal(t, rootKeyA, rootKeyB)
|
||||
|
||||
// Now also try with a delegation key
|
||||
releasesKey, err := getOrGenerateNotaryKey(notaryRepo, data.RoleName(trust.ReleasesRole))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, releasesKey)
|
||||
|
||||
// we should now have two keys
|
||||
allKeys = notaryRepo.GetCryptoService().ListAllKeys()
|
||||
assert.Len(t, allKeys, 2)
|
||||
assert.NotNil(t, notaryRepo.GetCryptoService().GetKey(releasesKey.ID()))
|
||||
// The key we retrieved should be identical to the one we generated
|
||||
assert.NotEqual(t, releasesKey, rootKeyA)
|
||||
assert.NotEqual(t, releasesKey, rootKeyB)
|
||||
}
|
||||
|
||||
func TestAddStageSigners(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)
|
||||
|
||||
// stage targets/user
|
||||
userRole := data.RoleName("targets/user")
|
||||
userKey := data.NewPublicKey("algoA", []byte("a"))
|
||||
addStagedSigner(notaryRepo, userRole, []data.PublicKey{userKey})
|
||||
// check the changelist for four total changes: two on targets/releases and two on targets/user
|
||||
cl, err := notaryRepo.GetChangelist()
|
||||
assert.NoError(t, err)
|
||||
changeList := cl.List()
|
||||
assert.Len(t, changeList, 4)
|
||||
// ordering is determinstic:
|
||||
|
||||
// first change is for targets/user key creation
|
||||
newSignerKeyChange := changeList[0]
|
||||
expectedJSON, err := json.Marshal(&changelist.TUFDelegation{
|
||||
NewThreshold: notary.MinThreshold,
|
||||
AddKeys: data.KeyList([]data.PublicKey{userKey}),
|
||||
})
|
||||
expectedChange := changelist.NewTUFChange(
|
||||
changelist.ActionCreate,
|
||||
userRole,
|
||||
changelist.TypeTargetsDelegation,
|
||||
"", // no path for delegations
|
||||
expectedJSON,
|
||||
)
|
||||
assert.Equal(t, expectedChange, newSignerKeyChange)
|
||||
|
||||
// second change is for targets/user getting all paths
|
||||
newSignerPathsChange := changeList[1]
|
||||
expectedJSON, err = json.Marshal(&changelist.TUFDelegation{
|
||||
AddPaths: []string{""},
|
||||
})
|
||||
expectedChange = changelist.NewTUFChange(
|
||||
changelist.ActionCreate,
|
||||
userRole,
|
||||
changelist.TypeTargetsDelegation,
|
||||
"", // no path for delegations
|
||||
expectedJSON,
|
||||
)
|
||||
assert.Equal(t, expectedChange, newSignerPathsChange)
|
||||
|
||||
releasesRole := data.RoleName("targets/releases")
|
||||
|
||||
// third change is for targets/releases key creation
|
||||
releasesKeyChange := changeList[2]
|
||||
expectedJSON, err = json.Marshal(&changelist.TUFDelegation{
|
||||
NewThreshold: notary.MinThreshold,
|
||||
AddKeys: data.KeyList([]data.PublicKey{userKey}),
|
||||
})
|
||||
expectedChange = changelist.NewTUFChange(
|
||||
changelist.ActionCreate,
|
||||
releasesRole,
|
||||
changelist.TypeTargetsDelegation,
|
||||
"", // no path for delegations
|
||||
expectedJSON,
|
||||
)
|
||||
assert.Equal(t, expectedChange, releasesKeyChange)
|
||||
|
||||
// fourth change is for targets/releases getting all paths
|
||||
releasesPathsChange := changeList[3]
|
||||
expectedJSON, err = json.Marshal(&changelist.TUFDelegation{
|
||||
AddPaths: []string{""},
|
||||
})
|
||||
expectedChange = changelist.NewTUFChange(
|
||||
changelist.ActionCreate,
|
||||
releasesRole,
|
||||
changelist.TypeTargetsDelegation,
|
||||
"", // no path for delegations
|
||||
expectedJSON,
|
||||
)
|
||||
assert.Equal(t, expectedChange, releasesPathsChange)
|
||||
}
|
||||
|
||||
func TestGetSignedManifestHashAndSize(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)
|
||||
target := &client.Target{}
|
||||
target.Hashes, target.Length, err = getSignedManifestHashAndSize(notaryRepo, "test")
|
||||
assert.EqualError(t, err, "client is offline")
|
||||
}
|
||||
|
||||
func TestGetReleasedTargetHashAndSize(t *testing.T) {
|
||||
oneReleasedTgt := []client.TargetSignedStruct{}
|
||||
// make and append 3 non-released signatures on the "unreleased" target
|
||||
unreleasedTgt := client.Target{Name: "unreleased", Hashes: data.Hashes{notary.SHA256: []byte("hash")}}
|
||||
for _, unreleasedRole := range []string{"targets/a", "targets/b", "targets/c"} {
|
||||
oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: unreleasedTgt})
|
||||
}
|
||||
_, _, err := getReleasedTargetHashAndSize(oneReleasedTgt, "unreleased")
|
||||
assert.EqualError(t, err, "No valid trust data for unreleased")
|
||||
releasedTgt := client.Target{Name: "released", Hashes: data.Hashes{notary.SHA256: []byte("released-hash")}}
|
||||
oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: releasedTgt})
|
||||
hash, _, _ := getReleasedTargetHashAndSize(oneReleasedTgt, "unreleased")
|
||||
assert.Equal(t, data.Hashes{notary.SHA256: []byte("released-hash")}, hash)
|
||||
|
||||
}
|
||||
|
||||
func TestCreateTarget(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 = createTarget(notaryRepo, "")
|
||||
assert.EqualError(t, err, "No tag specified")
|
||||
_, err = createTarget(notaryRepo, "1")
|
||||
assert.EqualError(t, err, "client is offline")
|
||||
}
|
||||
|
||||
func TestGetExistingSignatureInfoForReleasedTag(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 = getExistingSignatureInfoForReleasedTag(notaryRepo, "test")
|
||||
assert.EqualError(t, err, "client is offline")
|
||||
}
|
||||
|
||||
func TestPrettyPrintExistingSignatureInfo(t *testing.T) {
|
||||
fakeCli := test.NewFakeCli(&fakeClient{})
|
||||
|
||||
signers := []string{"Bob", "Alice", "Carol"}
|
||||
existingSig := trustTagRow{trustTagKey{"tagName", "abc123"}, signers}
|
||||
prettyPrintExistingSignatureInfo(fakeCli, existingSig)
|
||||
|
||||
assert.Contains(t, fakeCli.OutBuffer().String(), "Existing signatures for tag tagName digest abc123 from:\nAlice, Bob, Carol")
|
||||
}
|
||||
|
||||
func TestChangeList(t *testing.T) {
|
||||
tmpDir, err := ioutil.TempDir("", "docker-sign-test-")
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
config.SetDir(tmpDir)
|
||||
cmd := newSignCommand(
|
||||
test.NewFakeCli(&fakeClient{}))
|
||||
cmd.SetArgs([]string{"ubuntu:latest"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
err = cmd.Execute()
|
||||
notaryRepo, err := client.NewFileCachedRepository(tmpDir, "docker.io/library/ubuntu", "https://localhost", nil, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{})
|
||||
assert.NoError(t, err)
|
||||
cl, err := notaryRepo.GetChangelist()
|
||||
assert.Equal(t, len(cl.List()), 0)
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
SIGNED TAG DIGEST SIGNERS
|
||||
green 677265656e2d646967657374 (Repo Admin)
|
||||
|
||||
Administrative keys for signed-repo:
|
||||
Repository Key: targetsID
|
||||
Root Key: rootID
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
SIGNED TAG DIGEST SIGNERS
|
||||
blue 626c75652d646967657374 alice
|
||||
green 677265656e2d646967657374 (Repo Admin)
|
||||
red 7265642d646967657374 alice, bob
|
||||
|
||||
List of signers and their keys for signed-repo:
|
||||
|
||||
SIGNER KEYS
|
||||
alice A
|
||||
bob B
|
||||
|
||||
Administrative keys for signed-repo:
|
||||
Repository Key: targetsID
|
||||
Root Key: rootID
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
SIGNED TAG DIGEST SIGNERS
|
||||
green 677265656e2d646967657374 (Repo Admin)
|
||||
|
||||
Administrative keys for signed-repo:
|
||||
Repository Key: targetsID
|
||||
Root Key: rootID
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
|
||||
No signatures for signed-repo:unsigned
|
||||
|
||||
|
||||
List of signers and their keys for signed-repo:
|
||||
|
||||
SIGNER KEYS
|
||||
alice A
|
||||
bob B
|
||||
|
||||
Administrative keys for signed-repo:
|
||||
Repository Key: targetsID
|
||||
Root Key: rootID
|
||||
@@ -0,0 +1,229 @@
|
||||
package trust
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/notary"
|
||||
"github.com/docker/notary/client"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// trustTagKey represents a unique signed tag and hex-encoded hash pair
|
||||
type trustTagKey struct {
|
||||
TagName string
|
||||
HashHex string
|
||||
}
|
||||
|
||||
// trustTagRow encodes all human-consumable information for a signed tag, including signers
|
||||
type trustTagRow struct {
|
||||
trustTagKey
|
||||
Signers []string
|
||||
}
|
||||
|
||||
type trustTagRowList []trustTagRow
|
||||
|
||||
func (tagComparator trustTagRowList) Len() int {
|
||||
return len(tagComparator)
|
||||
}
|
||||
|
||||
func (tagComparator trustTagRowList) Less(i, j int) bool {
|
||||
return tagComparator[i].TagName < tagComparator[j].TagName
|
||||
}
|
||||
|
||||
func (tagComparator trustTagRowList) Swap(i, j int) {
|
||||
tagComparator[i], tagComparator[j] = tagComparator[j], tagComparator[i]
|
||||
}
|
||||
|
||||
func newViewCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "view [OPTIONS] IMAGE[:TAG]",
|
||||
Short: "Display detailed information about keys and signatures",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return lookupTrustInfo(dockerCli, args[0])
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func lookupTrustInfo(cli command.Cli, remote string) error {
|
||||
ctx := context.Background()
|
||||
authResolver := func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
|
||||
return command.ResolveAuthConfig(ctx, cli, index)
|
||||
}
|
||||
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, authResolver, remote)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tag := imgRefAndAuth.Tag()
|
||||
notaryRepo, err := cli.NotaryClient(*imgRefAndAuth, trust.ActionsPullOnly)
|
||||
if err != nil {
|
||||
return trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
|
||||
}
|
||||
|
||||
if err = clearChangeList(notaryRepo); err != nil {
|
||||
return err
|
||||
}
|
||||
defer clearChangeList(notaryRepo)
|
||||
|
||||
// Retrieve all released signatures, match them, and pretty print them
|
||||
allSignedTargets, err := notaryRepo.GetAllTargetMetadataByName(tag)
|
||||
if err != nil {
|
||||
logrus.Debug(trust.NotaryError(imgRefAndAuth.Reference().Name(), err))
|
||||
// print an empty table if we don't have signed targets, but have an initialized notary repo
|
||||
if _, ok := err.(client.ErrNoSuchTarget); !ok {
|
||||
return fmt.Errorf("No signatures or cannot access %s", remote)
|
||||
}
|
||||
}
|
||||
signatureRows := matchReleasedSignatures(allSignedTargets)
|
||||
if len(signatureRows) > 0 {
|
||||
if err := printSignatures(cli.Out(), signatureRows); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(cli.Out(), "\nNo signatures for %s\n\n", remote)
|
||||
}
|
||||
|
||||
// get the administrative roles
|
||||
adminRolesWithSigs, err := notaryRepo.ListRoles()
|
||||
if err != nil {
|
||||
return fmt.Errorf("No signers for %s", remote)
|
||||
}
|
||||
|
||||
// get delegation roles with the canonical key IDs
|
||||
delegationRoles, err := notaryRepo.GetDelegationRoles()
|
||||
if err != nil {
|
||||
logrus.Debugf("no delegation roles found, or error fetching them for %s: %v", remote, err)
|
||||
}
|
||||
signerRoleToKeyIDs := getDelegationRoleToKeyMap(delegationRoles)
|
||||
|
||||
// If we do not have additional signers, do not display
|
||||
if len(signerRoleToKeyIDs) > 0 {
|
||||
fmt.Fprintf(cli.Out(), "\nList of signers and their keys for %s:\n\n", strings.Split(remote, ":")[0])
|
||||
printSignerInfo(cli.Out(), signerRoleToKeyIDs)
|
||||
}
|
||||
|
||||
// This will always have the root and targets information
|
||||
fmt.Fprintf(cli.Out(), "\nAdministrative keys for %s:\n", strings.Split(remote, ":")[0])
|
||||
printSortedAdminKeys(cli.Out(), adminRolesWithSigs)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printSortedAdminKeys(out io.Writer, adminRoles []client.RoleWithSignatures) {
|
||||
sort.Slice(adminRoles, func(i, j int) bool { return adminRoles[i].Name > adminRoles[j].Name })
|
||||
for _, adminRole := range adminRoles {
|
||||
fmt.Fprintf(out, "%s", formatAdminRole(adminRole))
|
||||
}
|
||||
}
|
||||
|
||||
func formatAdminRole(roleWithSigs client.RoleWithSignatures) string {
|
||||
adminKeyList := roleWithSigs.KeyIDs
|
||||
sort.Strings(adminKeyList)
|
||||
|
||||
var role string
|
||||
switch roleWithSigs.Name {
|
||||
case data.CanonicalTargetsRole:
|
||||
role = "Repository Key"
|
||||
case data.CanonicalRootRole:
|
||||
role = "Root Key"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s:\t%s\n", role, strings.Join(adminKeyList, ", "))
|
||||
}
|
||||
|
||||
func getDelegationRoleToKeyMap(rawDelegationRoles []data.Role) map[string][]string {
|
||||
signerRoleToKeyIDs := make(map[string][]string)
|
||||
for _, delRole := range rawDelegationRoles {
|
||||
switch delRole.Name {
|
||||
case trust.ReleasesRole, data.CanonicalRootRole, data.CanonicalSnapshotRole, data.CanonicalTargetsRole, data.CanonicalTimestampRole:
|
||||
continue
|
||||
default:
|
||||
signerRoleToKeyIDs[notaryRoleToSigner(delRole.Name)] = delRole.KeyIDs
|
||||
}
|
||||
}
|
||||
return signerRoleToKeyIDs
|
||||
}
|
||||
|
||||
// aggregate all signers for a "released" hash+tagname pair. To be "released," the tag must have been
|
||||
// signed into the "targets" or "targets/releases" role. Output is sorted by tag name
|
||||
func matchReleasedSignatures(allTargets []client.TargetSignedStruct) trustTagRowList {
|
||||
signatureRows := trustTagRowList{}
|
||||
// do a first pass to get filter on tags signed into "targets" or "targets/releases"
|
||||
releasedTargetRows := map[trustTagKey][]string{}
|
||||
for _, tgt := range allTargets {
|
||||
if isReleasedTarget(tgt.Role.Name) {
|
||||
releasedKey := trustTagKey{tgt.Target.Name, hex.EncodeToString(tgt.Target.Hashes[notary.SHA256])}
|
||||
releasedTargetRows[releasedKey] = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
// now fill out all signers on released keys
|
||||
for _, tgt := range allTargets {
|
||||
targetKey := trustTagKey{tgt.Target.Name, hex.EncodeToString(tgt.Target.Hashes[notary.SHA256])}
|
||||
// only considered released targets
|
||||
if _, ok := releasedTargetRows[targetKey]; ok && !isReleasedTarget(tgt.Role.Name) {
|
||||
releasedTargetRows[targetKey] = append(releasedTargetRows[targetKey], notaryRoleToSigner(tgt.Role.Name))
|
||||
}
|
||||
}
|
||||
|
||||
// compile the final output as a sorted slice
|
||||
for targetKey, signers := range releasedTargetRows {
|
||||
signatureRows = append(signatureRows, trustTagRow{targetKey, signers})
|
||||
}
|
||||
sort.Sort(signatureRows)
|
||||
return signatureRows
|
||||
}
|
||||
|
||||
// pretty print with ordered rows
|
||||
func printSignatures(out io.Writer, signatureRows trustTagRowList) error {
|
||||
trustTagCtx := formatter.Context{
|
||||
Output: out,
|
||||
Format: formatter.NewTrustTagFormat(),
|
||||
}
|
||||
// convert the formatted type before printing
|
||||
formattedTags := []formatter.SignedTagInfo{}
|
||||
for _, sigRow := range signatureRows {
|
||||
formattedSigners := sigRow.Signers
|
||||
if len(formattedSigners) == 0 {
|
||||
formattedSigners = append(formattedSigners, fmt.Sprintf("(%s)", releasedRoleName))
|
||||
}
|
||||
formattedTags = append(formattedTags, formatter.SignedTagInfo{
|
||||
Name: sigRow.TagName,
|
||||
Digest: sigRow.HashHex,
|
||||
Signers: formattedSigners,
|
||||
})
|
||||
}
|
||||
return formatter.TrustTagWrite(trustTagCtx, formattedTags)
|
||||
}
|
||||
|
||||
func printSignerInfo(out io.Writer, roleToKeyIDs map[string][]string) error {
|
||||
signerInfoCtx := formatter.Context{
|
||||
Output: out,
|
||||
Format: formatter.NewSignerInfoFormat(),
|
||||
Trunc: true,
|
||||
}
|
||||
formattedSignerInfo := formatter.SignerInfoList{}
|
||||
for name, keyIDs := range roleToKeyIDs {
|
||||
formattedSignerInfo = append(formattedSignerInfo, formatter.SignerInfo{
|
||||
Name: name,
|
||||
Keys: keyIDs,
|
||||
})
|
||||
}
|
||||
sort.Sort(formattedSignerInfo)
|
||||
return formatter.SignerInfoWrite(signerInfoCtx, formattedSignerInfo)
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
package trust
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/docker/notary"
|
||||
"github.com/docker/notary/client"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/gotestyourself/gotestyourself/golden"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
dockerClient.Client
|
||||
}
|
||||
|
||||
func TestTrustInspectCommandErrors(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{"remote1", "remote2"},
|
||||
expectedError: "requires exactly 1 argument",
|
||||
},
|
||||
{
|
||||
name: "sha-reference",
|
||||
args: []string{"870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd"},
|
||||
expectedError: "invalid repository name",
|
||||
},
|
||||
{
|
||||
name: "invalid-img-reference",
|
||||
args: []string{"ALPINE"},
|
||||
expectedError: "invalid reference format",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := newViewCommand(
|
||||
test.NewFakeCli(&fakeClient{}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustInspectCommandOfflineErrors(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getOfflineNotaryRepository)
|
||||
cmd := newViewCommand(cli)
|
||||
cmd.SetArgs([]string{"nonexistent-reg-name.io/image"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image")
|
||||
|
||||
cli = test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getOfflineNotaryRepository)
|
||||
cmd = newViewCommand(cli)
|
||||
cmd.SetArgs([]string{"nonexistent-reg-name.io/image:tag"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image")
|
||||
}
|
||||
|
||||
func TestTrustInspectCommandUninitializedErrors(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getUninitializedNotaryRepository)
|
||||
cmd := newViewCommand(cli)
|
||||
cmd.SetArgs([]string{"reg/unsigned-img"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img")
|
||||
|
||||
cli = test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getUninitializedNotaryRepository)
|
||||
cmd = newViewCommand(cli)
|
||||
cmd.SetArgs([]string{"reg/unsigned-img:tag"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img:tag")
|
||||
}
|
||||
|
||||
func TestTrustInspectCommandEmptyNotaryRepoErrors(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
|
||||
cmd := newViewCommand(cli)
|
||||
cmd.SetArgs([]string{"reg/img:unsigned-tag"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
assert.Contains(t, cli.OutBuffer().String(), "No signatures for reg/img:unsigned-tag")
|
||||
assert.Contains(t, cli.OutBuffer().String(), "Administrative keys for reg/img:")
|
||||
|
||||
cli = test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
|
||||
cmd = newViewCommand(cli)
|
||||
cmd.SetArgs([]string{"reg/img"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
assert.Contains(t, cli.OutBuffer().String(), "No signatures for reg/img")
|
||||
assert.Contains(t, cli.OutBuffer().String(), "Administrative keys for reg/img:")
|
||||
}
|
||||
|
||||
func TestTrustInspectCommandFullRepoWithoutSigners(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getLoadedWithNoSignersNotaryRepository)
|
||||
cmd := newViewCommand(cli)
|
||||
cmd.SetArgs([]string{"signed-repo"})
|
||||
assert.NoError(t, cmd.Execute())
|
||||
|
||||
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-full-repo-no-signers.golden")
|
||||
}
|
||||
|
||||
func TestTrustInspectCommandOneTagWithoutSigners(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getLoadedWithNoSignersNotaryRepository)
|
||||
cmd := newViewCommand(cli)
|
||||
cmd.SetArgs([]string{"signed-repo:green"})
|
||||
assert.NoError(t, cmd.Execute())
|
||||
|
||||
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-one-tag-no-signers.golden")
|
||||
}
|
||||
|
||||
func TestTrustInspectCommandFullRepoWithSigners(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getLoadedNotaryRepository)
|
||||
cmd := newViewCommand(cli)
|
||||
cmd.SetArgs([]string{"signed-repo"})
|
||||
assert.NoError(t, cmd.Execute())
|
||||
|
||||
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-full-repo-with-signers.golden")
|
||||
}
|
||||
|
||||
func TestTrustInspectCommandUnsignedTagInSignedRepo(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getLoadedNotaryRepository)
|
||||
cmd := newViewCommand(cli)
|
||||
cmd.SetArgs([]string{"signed-repo:unsigned"})
|
||||
assert.NoError(t, cmd.Execute())
|
||||
|
||||
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-unsigned-tag-with-signers.golden")
|
||||
}
|
||||
|
||||
func TestNotaryRoleToSigner(t *testing.T) {
|
||||
assert.Equal(t, releasedRoleName, notaryRoleToSigner(data.CanonicalTargetsRole))
|
||||
assert.Equal(t, releasedRoleName, notaryRoleToSigner(trust.ReleasesRole))
|
||||
assert.Equal(t, "signer", notaryRoleToSigner("targets/signer"))
|
||||
assert.Equal(t, "docker/signer", notaryRoleToSigner("targets/docker/signer"))
|
||||
|
||||
// It's nonsense for other base roles to have signed off on a target, but this function leaves role names intact
|
||||
for _, role := range data.BaseRoles {
|
||||
if role == data.CanonicalTargetsRole {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, role.String(), notaryRoleToSigner(role))
|
||||
}
|
||||
assert.Equal(t, "notarole", notaryRoleToSigner(data.RoleName("notarole")))
|
||||
}
|
||||
|
||||
// check if a role name is "released": either targets/releases or targets TUF roles
|
||||
func TestIsReleasedTarget(t *testing.T) {
|
||||
assert.True(t, isReleasedTarget(trust.ReleasesRole))
|
||||
for _, role := range data.BaseRoles {
|
||||
assert.Equal(t, role == data.CanonicalTargetsRole, isReleasedTarget(role))
|
||||
}
|
||||
assert.False(t, isReleasedTarget(data.RoleName("targets/not-releases")))
|
||||
assert.False(t, isReleasedTarget(data.RoleName("random")))
|
||||
assert.False(t, isReleasedTarget(data.RoleName("targets/releases/subrole")))
|
||||
}
|
||||
|
||||
// creates a mock delegation with a given name and no keys
|
||||
func mockDelegationRoleWithName(name string) data.DelegationRole {
|
||||
baseRole := data.NewBaseRole(
|
||||
data.RoleName(name),
|
||||
notary.MinThreshold,
|
||||
)
|
||||
return data.DelegationRole{baseRole, []string{}}
|
||||
}
|
||||
|
||||
func TestMatchEmptySignatures(t *testing.T) {
|
||||
// first try empty targets
|
||||
emptyTgts := []client.TargetSignedStruct{}
|
||||
|
||||
matchedSigRows := matchReleasedSignatures(emptyTgts)
|
||||
assert.Empty(t, matchedSigRows)
|
||||
}
|
||||
|
||||
func TestMatchUnreleasedSignatures(t *testing.T) {
|
||||
// try an "unreleased" target with 3 signatures, 0 rows will appear
|
||||
unreleasedTgts := []client.TargetSignedStruct{}
|
||||
|
||||
tgt := client.Target{Name: "unreleased", Hashes: data.Hashes{notary.SHA256: []byte("hash")}}
|
||||
for _, unreleasedRole := range []string{"targets/a", "targets/b", "targets/c"} {
|
||||
unreleasedTgts = append(unreleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: tgt})
|
||||
}
|
||||
|
||||
matchedSigRows := matchReleasedSignatures(unreleasedTgts)
|
||||
assert.Empty(t, matchedSigRows)
|
||||
}
|
||||
|
||||
func TestMatchOneReleasedSingleSignature(t *testing.T) {
|
||||
// now try only 1 "released" target with no additional sigs, 1 row will appear with 0 signers
|
||||
oneReleasedTgt := []client.TargetSignedStruct{}
|
||||
|
||||
// make and append the "released" target to our mock input
|
||||
releasedTgt := client.Target{Name: "released", Hashes: data.Hashes{notary.SHA256: []byte("released-hash")}}
|
||||
oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: releasedTgt})
|
||||
|
||||
// make and append 3 non-released signatures on the "unreleased" target
|
||||
unreleasedTgt := client.Target{Name: "unreleased", Hashes: data.Hashes{notary.SHA256: []byte("hash")}}
|
||||
for _, unreleasedRole := range []string{"targets/a", "targets/b", "targets/c"} {
|
||||
oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: unreleasedTgt})
|
||||
}
|
||||
|
||||
matchedSigRows := matchReleasedSignatures(oneReleasedTgt)
|
||||
assert.Len(t, matchedSigRows, 1)
|
||||
|
||||
outputRow := matchedSigRows[0]
|
||||
// Empty signers because "targets/releases" doesn't show up
|
||||
assert.Empty(t, outputRow.Signers)
|
||||
assert.Equal(t, releasedTgt.Name, outputRow.TagName)
|
||||
assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.HashHex)
|
||||
}
|
||||
|
||||
func TestMatchOneReleasedMultiSignature(t *testing.T) {
|
||||
// now try only 1 "released" target with 3 additional sigs, 1 row will appear with 3 signers
|
||||
oneReleasedTgt := []client.TargetSignedStruct{}
|
||||
|
||||
// make and append the "released" target to our mock input
|
||||
releasedTgt := client.Target{Name: "released", Hashes: data.Hashes{notary.SHA256: []byte("released-hash")}}
|
||||
oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: releasedTgt})
|
||||
|
||||
// make and append 3 non-released signatures on both the "released" and "unreleased" targets
|
||||
unreleasedTgt := client.Target{Name: "unreleased", Hashes: data.Hashes{notary.SHA256: []byte("hash")}}
|
||||
for _, unreleasedRole := range []string{"targets/a", "targets/b", "targets/c"} {
|
||||
oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: unreleasedTgt})
|
||||
oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: releasedTgt})
|
||||
}
|
||||
|
||||
matchedSigRows := matchReleasedSignatures(oneReleasedTgt)
|
||||
assert.Len(t, matchedSigRows, 1)
|
||||
|
||||
outputRow := matchedSigRows[0]
|
||||
// We should have three signers
|
||||
assert.Equal(t, outputRow.Signers, []string{"a", "b", "c"})
|
||||
assert.Equal(t, releasedTgt.Name, outputRow.TagName)
|
||||
assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.HashHex)
|
||||
}
|
||||
|
||||
func TestMatchMultiReleasedMultiSignature(t *testing.T) {
|
||||
// now try 3 "released" targets with additional sigs to show 3 rows as follows:
|
||||
// target-a is signed by targets/releases and targets/a - a will be the signer
|
||||
// target-b is signed by targets/releases, targets/a, targets/b - a and b will be the signers
|
||||
// target-c is signed by targets/releases, targets/a, targets/b, targets/c - a, b, and c will be the signers
|
||||
multiReleasedTgts := []client.TargetSignedStruct{}
|
||||
// make target-a, target-b, and target-c
|
||||
targetA := client.Target{Name: "target-a", Hashes: data.Hashes{notary.SHA256: []byte("target-a-hash")}}
|
||||
targetB := client.Target{Name: "target-b", Hashes: data.Hashes{notary.SHA256: []byte("target-b-hash")}}
|
||||
targetC := client.Target{Name: "target-c", Hashes: data.Hashes{notary.SHA256: []byte("target-c-hash")}}
|
||||
|
||||
// have targets/releases "sign" on all of these targets so they are released
|
||||
multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: targetA})
|
||||
multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: targetB})
|
||||
multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: targetC})
|
||||
|
||||
// targets/a signs off on all three targets (target-a, target-b, target-c):
|
||||
for _, tgt := range []client.Target{targetA, targetB, targetC} {
|
||||
multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/a"), Target: tgt})
|
||||
}
|
||||
|
||||
// targets/b signs off on the final two targets (target-b, target-c):
|
||||
for _, tgt := range []client.Target{targetB, targetC} {
|
||||
multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/b"), Target: tgt})
|
||||
}
|
||||
|
||||
// targets/c only signs off on the last target (target-c):
|
||||
multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/c"), Target: targetC})
|
||||
|
||||
matchedSigRows := matchReleasedSignatures(multiReleasedTgts)
|
||||
assert.Len(t, matchedSigRows, 3)
|
||||
|
||||
// note that the output is sorted by tag name, so we can reliably index to validate data:
|
||||
outputTargetA := matchedSigRows[0]
|
||||
assert.Equal(t, outputTargetA.Signers, []string{"a"})
|
||||
assert.Equal(t, targetA.Name, outputTargetA.TagName)
|
||||
assert.Equal(t, hex.EncodeToString(targetA.Hashes[notary.SHA256]), outputTargetA.HashHex)
|
||||
|
||||
outputTargetB := matchedSigRows[1]
|
||||
assert.Equal(t, outputTargetB.Signers, []string{"a", "b"})
|
||||
assert.Equal(t, targetB.Name, outputTargetB.TagName)
|
||||
assert.Equal(t, hex.EncodeToString(targetB.Hashes[notary.SHA256]), outputTargetB.HashHex)
|
||||
|
||||
outputTargetC := matchedSigRows[2]
|
||||
assert.Equal(t, outputTargetC.Signers, []string{"a", "b", "c"})
|
||||
assert.Equal(t, targetC.Name, outputTargetC.TagName)
|
||||
assert.Equal(t, hex.EncodeToString(targetC.Hashes[notary.SHA256]), outputTargetC.HashHex)
|
||||
}
|
||||
|
||||
func TestMatchReleasedSignatureFromTargets(t *testing.T) {
|
||||
// now try only 1 "released" target with no additional sigs, one rows will appear
|
||||
oneReleasedTgt := []client.TargetSignedStruct{}
|
||||
// make and append the "released" target to our mock input
|
||||
releasedTgt := client.Target{Name: "released", Hashes: data.Hashes{notary.SHA256: []byte("released-hash")}}
|
||||
oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(data.CanonicalTargetsRole.String()), Target: releasedTgt})
|
||||
matchedSigRows := matchReleasedSignatures(oneReleasedTgt)
|
||||
assert.Len(t, matchedSigRows, 1)
|
||||
outputRow := matchedSigRows[0]
|
||||
// Empty signers because "targets" doesn't show up
|
||||
assert.Empty(t, outputRow.Signers)
|
||||
assert.Equal(t, releasedTgt.Name, outputRow.TagName)
|
||||
assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.HashHex)
|
||||
}
|
||||
|
||||
func TestGetSignerRolesWithKeyIDs(t *testing.T) {
|
||||
roles := []data.Role{
|
||||
{
|
||||
RootRole: data.RootRole{
|
||||
KeyIDs: []string{"key11"},
|
||||
},
|
||||
Name: "targets/alice",
|
||||
},
|
||||
{
|
||||
RootRole: data.RootRole{
|
||||
KeyIDs: []string{"key21", "key22"},
|
||||
},
|
||||
Name: "targets/releases",
|
||||
},
|
||||
{
|
||||
RootRole: data.RootRole{
|
||||
KeyIDs: []string{"key31"},
|
||||
},
|
||||
Name: data.CanonicalTargetsRole,
|
||||
},
|
||||
{
|
||||
RootRole: data.RootRole{
|
||||
KeyIDs: []string{"key41", "key01"},
|
||||
},
|
||||
Name: data.CanonicalRootRole,
|
||||
},
|
||||
{
|
||||
RootRole: data.RootRole{
|
||||
KeyIDs: []string{"key51"},
|
||||
},
|
||||
Name: data.CanonicalSnapshotRole,
|
||||
},
|
||||
{
|
||||
RootRole: data.RootRole{
|
||||
KeyIDs: []string{"key61"},
|
||||
},
|
||||
Name: data.CanonicalTimestampRole,
|
||||
},
|
||||
{
|
||||
RootRole: data.RootRole{
|
||||
KeyIDs: []string{"key71", "key72"},
|
||||
},
|
||||
Name: "targets/bob",
|
||||
},
|
||||
}
|
||||
expectedSignerRoleToKeyIDs := map[string][]string{
|
||||
"alice": {"key11"},
|
||||
"bob": {"key71", "key72"},
|
||||
}
|
||||
|
||||
var roleWithSigs []client.RoleWithSignatures
|
||||
for _, role := range roles {
|
||||
roleWithSig := client.RoleWithSignatures{Role: role, Signatures: nil}
|
||||
roleWithSigs = append(roleWithSigs, roleWithSig)
|
||||
}
|
||||
signerRoleToKeyIDs := getDelegationRoleToKeyMap(roles)
|
||||
assert.Equal(t, expectedSignerRoleToKeyIDs, signerRoleToKeyIDs)
|
||||
}
|
||||
|
||||
func TestFormatAdminRole(t *testing.T) {
|
||||
aliceRole := data.Role{
|
||||
RootRole: data.RootRole{
|
||||
KeyIDs: []string{"key11"},
|
||||
},
|
||||
Name: "targets/alice",
|
||||
}
|
||||
aliceRoleWithSigs := client.RoleWithSignatures{Role: aliceRole, Signatures: nil}
|
||||
assert.Equal(t, "", formatAdminRole(aliceRoleWithSigs))
|
||||
|
||||
releasesRole := data.Role{
|
||||
RootRole: data.RootRole{
|
||||
KeyIDs: []string{"key11"},
|
||||
},
|
||||
Name: "targets/releases",
|
||||
}
|
||||
releasesRoleWithSigs := client.RoleWithSignatures{Role: releasesRole, Signatures: nil}
|
||||
assert.Equal(t, "", formatAdminRole(releasesRoleWithSigs))
|
||||
|
||||
timestampRole := data.Role{
|
||||
RootRole: data.RootRole{
|
||||
KeyIDs: []string{"key11"},
|
||||
},
|
||||
Name: data.CanonicalTimestampRole,
|
||||
}
|
||||
timestampRoleWithSigs := client.RoleWithSignatures{Role: timestampRole, Signatures: nil}
|
||||
assert.Equal(t, "", formatAdminRole(timestampRoleWithSigs))
|
||||
|
||||
snapshotRole := data.Role{
|
||||
RootRole: data.RootRole{
|
||||
KeyIDs: []string{"key11"},
|
||||
},
|
||||
Name: data.CanonicalSnapshotRole,
|
||||
}
|
||||
snapshotRoleWithSigs := client.RoleWithSignatures{Role: snapshotRole, Signatures: nil}
|
||||
assert.Equal(t, "", formatAdminRole(snapshotRoleWithSigs))
|
||||
|
||||
rootRole := data.Role{
|
||||
RootRole: data.RootRole{
|
||||
KeyIDs: []string{"key11"},
|
||||
},
|
||||
Name: data.CanonicalRootRole,
|
||||
}
|
||||
rootRoleWithSigs := client.RoleWithSignatures{Role: rootRole, Signatures: nil}
|
||||
assert.Equal(t, "Root Key:\tkey11\n", formatAdminRole(rootRoleWithSigs))
|
||||
|
||||
targetsRole := data.Role{
|
||||
RootRole: data.RootRole{
|
||||
KeyIDs: []string{"key99", "abc", "key11"},
|
||||
},
|
||||
Name: data.CanonicalTargetsRole,
|
||||
}
|
||||
targetsRoleWithSigs := client.RoleWithSignatures{Role: targetsRole, Signatures: nil}
|
||||
assert.Equal(t, "Repository Key:\tabc, key11, key99\n", formatAdminRole(targetsRoleWithSigs))
|
||||
}
|
||||
@@ -221,6 +221,7 @@ func createTransformHook() mapstructure.DecodeHookFuncType {
|
||||
reflect.TypeOf(types.Labels{}): transformMappingOrListFunc("=", false),
|
||||
reflect.TypeOf(types.MappingWithColon{}): transformMappingOrListFunc(":", false),
|
||||
reflect.TypeOf(types.ServiceVolumeConfig{}): transformServiceVolumeConfig,
|
||||
reflect.TypeOf(types.BuildConfig{}): transformBuildConfig,
|
||||
}
|
||||
|
||||
return func(_ reflect.Type, target reflect.Type, data interface{}) (interface{}, error) {
|
||||
@@ -563,6 +564,17 @@ func transformStringSourceMap(data interface{}) (interface{}, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func transformBuildConfig(data interface{}) (interface{}, error) {
|
||||
switch value := data.(type) {
|
||||
case string:
|
||||
return map[string]interface{}{"context": value}, nil
|
||||
case map[string]interface{}:
|
||||
return data, nil
|
||||
default:
|
||||
return data, errors.Errorf("invalid type %T for service build", value)
|
||||
}
|
||||
}
|
||||
|
||||
func transformServiceVolumeConfig(data interface{}) (interface{}, error) {
|
||||
switch value := data.(type) {
|
||||
case string:
|
||||
|
||||
@@ -528,6 +528,26 @@ services:
|
||||
assert.Equal(t, []string{"build", "links"}, unsupported)
|
||||
}
|
||||
|
||||
func TestBuildProperties(t *testing.T) {
|
||||
dict, err := ParseYAML([]byte(`
|
||||
version: "3"
|
||||
services:
|
||||
web:
|
||||
image: web
|
||||
build: .
|
||||
links:
|
||||
- bar
|
||||
db:
|
||||
image: db
|
||||
build:
|
||||
context: ./db
|
||||
`))
|
||||
require.NoError(t, err)
|
||||
configDetails := buildConfigDetails(dict, nil)
|
||||
_, err = Load(configDetails)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDeprecatedProperties(t *testing.T) {
|
||||
dict, err := ParseYAML([]byte(`
|
||||
version: "3"
|
||||
|
||||
@@ -7,13 +7,15 @@ import (
|
||||
// DetectDefaultStore return the default credentials store for the platform if
|
||||
// the store executable is available.
|
||||
func DetectDefaultStore(store string) string {
|
||||
platformDefault := defaultCredentialsStore()
|
||||
|
||||
// user defined or no default for platform
|
||||
if store != "" || defaultCredentialsStore == "" {
|
||||
if store != "" || platformDefault == "" {
|
||||
return store
|
||||
}
|
||||
|
||||
if _, err := exec.LookPath(remoteCredentialsPrefix + defaultCredentialsStore); err == nil {
|
||||
return defaultCredentialsStore
|
||||
if _, err := exec.LookPath(remoteCredentialsPrefix + platformDefault); err == nil {
|
||||
return platformDefault
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
package credentials
|
||||
|
||||
const defaultCredentialsStore = "osxkeychain"
|
||||
func defaultCredentialsStore() string {
|
||||
return "osxkeychain"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
package credentials
|
||||
|
||||
const defaultCredentialsStore = "secretservice"
|
||||
import (
|
||||
"github.com/docker/docker-credential-helpers/pass"
|
||||
)
|
||||
|
||||
func defaultCredentialsStore() string {
|
||||
if pass.PassInitialized {
|
||||
return "pass"
|
||||
}
|
||||
|
||||
return "secretservice"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
package credentials
|
||||
|
||||
const defaultCredentialsStore = "wincred"
|
||||
func defaultCredentialsStore() string {
|
||||
return "wincred"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package trust
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -10,8 +12,8 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
cliconfig "github.com/docker/cli/cli/config"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/distribution/registry/client/auth"
|
||||
"github.com/docker/distribution/registry/client/auth/challenge"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
@@ -27,13 +29,18 @@ import (
|
||||
"github.com/docker/notary/trustpinning"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/docker/notary/tuf/signed"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
// ReleasesRole is the role named "releases"
|
||||
ReleasesRole = path.Join(data.CanonicalTargetsRole, "releases")
|
||||
ReleasesRole = data.RoleName(path.Join(data.CanonicalTargetsRole.String(), "releases"))
|
||||
// ActionsPullOnly defines the actions for read-only interactions with a Notary Repository
|
||||
ActionsPullOnly = []string{"pull"}
|
||||
// ActionsPushAndPull defines the actions for read-write interactions with a Notary Repository
|
||||
ActionsPushAndPull = []string{"pull", "push"}
|
||||
)
|
||||
|
||||
func trustDirectory() string {
|
||||
@@ -86,7 +93,7 @@ func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) {
|
||||
// GetNotaryRepository returns a NotaryRepository which stores all the
|
||||
// information needed to operate on a notary repository.
|
||||
// It creates an HTTP transport providing authentication support.
|
||||
func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryInfo, authConfig types.AuthConfig, actions ...string) (*client.NotaryRepository, error) {
|
||||
func GetNotaryRepository(in io.Reader, out io.Writer, userAgent string, repoInfo *registry.RepositoryInfo, authConfig *types.AuthConfig, actions ...string) (client.Repository, error) {
|
||||
server, err := Server(repoInfo.Index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -119,7 +126,7 @@ func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryI
|
||||
}
|
||||
|
||||
// Skip configuration headers since request is not going to Docker daemon
|
||||
modifiers := registry.DockerHeaders(command.UserAgent(), http.Header{})
|
||||
modifiers := registry.DockerHeaders(userAgent, http.Header{})
|
||||
authTransport := transport.NewTransport(base, modifiers...)
|
||||
pingClient := &http.Client{
|
||||
Transport: authTransport,
|
||||
@@ -152,7 +159,7 @@ func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryI
|
||||
Actions: actions,
|
||||
Class: repoInfo.Class,
|
||||
}
|
||||
creds := simpleCredentialStore{auth: authConfig}
|
||||
creds := simpleCredentialStore{auth: *authConfig}
|
||||
tokenHandlerOptions := auth.TokenHandlerOptions{
|
||||
Transport: authTransport,
|
||||
Credentials: creds,
|
||||
@@ -164,23 +171,23 @@ func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryI
|
||||
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
|
||||
tr := transport.NewTransport(base, modifiers...)
|
||||
|
||||
return client.NewNotaryRepository(
|
||||
return client.NewFileCachedRepository(
|
||||
trustDirectory(),
|
||||
repoInfo.Name.Name(),
|
||||
data.GUN(repoInfo.Name.Name()),
|
||||
server,
|
||||
tr,
|
||||
getPassphraseRetriever(streams),
|
||||
getPassphraseRetriever(in, out),
|
||||
trustpinning.TrustPinConfig{})
|
||||
}
|
||||
|
||||
func getPassphraseRetriever(streams command.Streams) notary.PassRetriever {
|
||||
func getPassphraseRetriever(in io.Reader, out io.Writer) notary.PassRetriever {
|
||||
aliasMap := map[string]string{
|
||||
"root": "root",
|
||||
"snapshot": "repository",
|
||||
"targets": "repository",
|
||||
"default": "repository",
|
||||
}
|
||||
baseRetriever := passphrase.PromptRetrieverWithInOut(streams.In(), streams.Out(), aliasMap)
|
||||
baseRetriever := passphrase.PromptRetrieverWithInOut(in, out, aliasMap)
|
||||
env := map[string]string{
|
||||
"root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"),
|
||||
"snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"),
|
||||
@@ -193,7 +200,7 @@ func getPassphraseRetriever(streams command.Streams) notary.PassRetriever {
|
||||
return v, numAttempts > 1, nil
|
||||
}
|
||||
// For non-root roles, we can also try the "default" alias if it is specified
|
||||
if v := env["default"]; v != "" && alias != data.CanonicalRootRole {
|
||||
if v := env["default"]; v != "" && alias != data.CanonicalRootRole.String() {
|
||||
return v, numAttempts > 1, nil
|
||||
}
|
||||
return baseRetriever(keyName, alias, createNew, numAttempts)
|
||||
@@ -230,3 +237,130 @@ func NotaryError(repoName string, err error) error {
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetSignableRoles returns a list of roles for which we have valid signing
|
||||
// keys, given a notary repository and a target
|
||||
func GetSignableRoles(repo client.Repository, target *client.Target) ([]data.RoleName, error) {
|
||||
var signableRoles []data.RoleName
|
||||
|
||||
// translate the full key names, which includes the GUN, into just the key IDs
|
||||
allCanonicalKeyIDs := make(map[string]struct{})
|
||||
for fullKeyID := range repo.GetCryptoService().ListAllKeys() {
|
||||
allCanonicalKeyIDs[path.Base(fullKeyID)] = struct{}{}
|
||||
}
|
||||
|
||||
allDelegationRoles, err := repo.GetDelegationRoles()
|
||||
if err != nil {
|
||||
return signableRoles, err
|
||||
}
|
||||
|
||||
// if there are no delegation roles, then just try to sign it into the targets role
|
||||
if len(allDelegationRoles) == 0 {
|
||||
signableRoles = append(signableRoles, data.CanonicalTargetsRole)
|
||||
return signableRoles, nil
|
||||
}
|
||||
|
||||
// there are delegation roles, find every delegation role we have a key for, and
|
||||
// attempt to sign into into all those roles.
|
||||
for _, delegationRole := range allDelegationRoles {
|
||||
// We do not support signing any delegation role that isn't a direct child of the targets role.
|
||||
// Also don't bother checking the keys if we can't add the target
|
||||
// to this role due to path restrictions
|
||||
if path.Dir(delegationRole.Name.String()) != data.CanonicalTargetsRole.String() || !delegationRole.CheckPaths(target.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, canonicalKeyID := range delegationRole.KeyIDs {
|
||||
if _, ok := allCanonicalKeyIDs[canonicalKeyID]; ok {
|
||||
signableRoles = append(signableRoles, delegationRole.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(signableRoles) == 0 {
|
||||
return signableRoles, errors.Errorf("no valid signing keys for delegation roles")
|
||||
}
|
||||
|
||||
return signableRoles, nil
|
||||
|
||||
}
|
||||
|
||||
// ImageRefAndAuth contains all reference information and the auth config for an image request
|
||||
type ImageRefAndAuth struct {
|
||||
authConfig *types.AuthConfig
|
||||
reference reference.Named
|
||||
repoInfo *registry.RepositoryInfo
|
||||
tag string
|
||||
digest digest.Digest
|
||||
}
|
||||
|
||||
// NewImageRefAndAuth creates a new ImageRefAndAuth struct
|
||||
func NewImageRefAndAuth(authConfig *types.AuthConfig, reference reference.Named, repoInfo *registry.RepositoryInfo, tag string, digest digest.Digest) *ImageRefAndAuth {
|
||||
return &ImageRefAndAuth{authConfig, reference, repoInfo, tag, digest}
|
||||
}
|
||||
|
||||
// GetImageReferencesAndAuth retrieves the necessary reference and auth information for an image name
|
||||
// as a ImageRefAndAuth struct
|
||||
func GetImageReferencesAndAuth(ctx context.Context, authResolver func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig, imgName string) (*ImageRefAndAuth, error) {
|
||||
ref, err := reference.ParseNormalizedNamed(imgName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Resolve the Repository name from fqn to RepositoryInfo
|
||||
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authConfig := authResolver(ctx, repoInfo.Index)
|
||||
return NewImageRefAndAuth(&authConfig, ref, repoInfo, getTag(ref), getDigest(ref)), nil
|
||||
}
|
||||
|
||||
func getTag(ref reference.Named) string {
|
||||
switch x := ref.(type) {
|
||||
case reference.Canonical, reference.Digested:
|
||||
return ""
|
||||
case reference.NamedTagged:
|
||||
return x.Tag()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func getDigest(ref reference.Named) digest.Digest {
|
||||
switch x := ref.(type) {
|
||||
case reference.Canonical:
|
||||
return x.Digest()
|
||||
case reference.Digested:
|
||||
return x.Digest()
|
||||
default:
|
||||
return digest.Digest("")
|
||||
}
|
||||
}
|
||||
|
||||
// AuthConfig returns the auth information (username, etc) for a given ImageRefAndAuth
|
||||
func (imgRefAuth *ImageRefAndAuth) AuthConfig() *types.AuthConfig {
|
||||
return imgRefAuth.authConfig
|
||||
}
|
||||
|
||||
// Reference returns the Image reference for a given ImageRefAndAuth
|
||||
func (imgRefAuth *ImageRefAndAuth) Reference() reference.Named {
|
||||
return imgRefAuth.reference
|
||||
}
|
||||
|
||||
// RepoInfo returns the repository information for a given ImageRefAndAuth
|
||||
func (imgRefAuth *ImageRefAndAuth) RepoInfo() *registry.RepositoryInfo {
|
||||
return imgRefAuth.repoInfo
|
||||
}
|
||||
|
||||
// Tag returns the Image tag for a given ImageRefAndAuth
|
||||
func (imgRefAuth *ImageRefAndAuth) Tag() string {
|
||||
return imgRefAuth.tag
|
||||
}
|
||||
|
||||
// Digest returns the Image digest for a given ImageRefAndAuth
|
||||
func (imgRefAuth *ImageRefAndAuth) Digest() digest.Digest {
|
||||
return imgRefAuth.digest
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package trust
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/notary/client"
|
||||
"github.com/docker/notary/passphrase"
|
||||
"github.com/docker/notary/trustpinning"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetTag(t *testing.T) {
|
||||
ref, err := reference.ParseNormalizedNamed("ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2")
|
||||
assert.NoError(t, err)
|
||||
tag := getTag(ref)
|
||||
assert.Equal(t, "", tag)
|
||||
|
||||
ref, err = reference.ParseNormalizedNamed("alpine:latest")
|
||||
assert.NoError(t, err)
|
||||
tag = getTag(ref)
|
||||
assert.Equal(t, tag, "latest")
|
||||
|
||||
ref, err = reference.ParseNormalizedNamed("alpine")
|
||||
assert.NoError(t, err)
|
||||
tag = getTag(ref)
|
||||
assert.Equal(t, tag, "")
|
||||
}
|
||||
|
||||
func TestGetDigest(t *testing.T) {
|
||||
ref, err := reference.ParseNormalizedNamed("ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2")
|
||||
assert.NoError(t, err)
|
||||
d := getDigest(ref)
|
||||
assert.Equal(t, digest.Digest("sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2"), d)
|
||||
|
||||
ref, err = reference.ParseNormalizedNamed("alpine:latest")
|
||||
assert.NoError(t, err)
|
||||
d = getDigest(ref)
|
||||
assert.Equal(t, digest.Digest(""), d)
|
||||
|
||||
ref, err = reference.ParseNormalizedNamed("alpine")
|
||||
assert.NoError(t, err)
|
||||
d = getDigest(ref)
|
||||
assert.Equal(t, digest.Digest(""), d)
|
||||
}
|
||||
|
||||
func TestGetSignableRolesError(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("password"), trustpinning.TrustPinConfig{})
|
||||
target := client.Target{}
|
||||
_, err = GetSignableRoles(notaryRepo, &target)
|
||||
assert.EqualError(t, err, "client is offline")
|
||||
}
|
||||
@@ -3294,7 +3294,6 @@ _docker_service_update() {
|
||||
# and `docker service update`
|
||||
_docker_service_update_and_create() {
|
||||
local options_with_args="
|
||||
--credential-spec
|
||||
--endpoint-mode
|
||||
--entrypoint
|
||||
--env -e
|
||||
@@ -3335,6 +3334,9 @@ _docker_service_update_and_create() {
|
||||
--user -u
|
||||
--workdir -w
|
||||
"
|
||||
__docker_daemon_os_is windows && options_with_args+="
|
||||
--credential-spec
|
||||
"
|
||||
|
||||
local boolean_options="
|
||||
--detach=false -d=false
|
||||
@@ -4292,14 +4294,14 @@ _docker_search() {
|
||||
__docker_nospace
|
||||
return
|
||||
;;
|
||||
--limit)
|
||||
--format|--limit)
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--filter --help --limit --no-trunc" -- "$cur" ) )
|
||||
COMPREPLY=( $( compgen -W "--filter -f --format --help --limit --no-trunc" -- "$cur" ) )
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -54,6 +54,14 @@ test: test-unit test-e2e
|
||||
cross: build_cross_image
|
||||
docker run --rm $(ENVVARS) $(MOUNTS) $(CROSS_IMAGE_NAME) make cross
|
||||
|
||||
.PHONY: binary-windows
|
||||
binary-windows: build_cross_image
|
||||
docker run --rm $(ENVVARS) $(MOUNTS) $(CROSS_IMAGE_NAME) make $@
|
||||
|
||||
.PHONY: binary-osx
|
||||
binary-osx: build_cross_image
|
||||
docker run --rm $(ENVVARS) $(MOUNTS) $(CROSS_IMAGE_NAME) make $@
|
||||
|
||||
.PHONY: watch
|
||||
watch: build_docker_image
|
||||
docker run --rm $(ENVVARS) $(MOUNTS) $(DEV_DOCKER_IMAGE_NAME) make watch
|
||||
|
||||
@@ -184,7 +184,7 @@ The currently supported filters are:
|
||||
* plugin (`plugin=<name or id>`)
|
||||
* scope (`scope=<local or swarm>`)
|
||||
* type (`type=<container or image or volume or network or daemon or plugin or service or node or secret or config>`)
|
||||
* volume (`volume=<name or id>`)
|
||||
* volume (`volume=<name>`)
|
||||
|
||||
#### Format
|
||||
|
||||
|
||||
@@ -63,8 +63,9 @@ $ cat ~/my_password.txt | docker login --username foo --password-stdin
|
||||
2. user is added to the `docker` group. This will impact the security of your system; the `docker` group is `root` equivalent. See [Docker Daemon Attack Surface](https://docs.docker.com/security/security/#docker-daemon-attack-surface) for details.
|
||||
|
||||
You can log into any public or private repository for which you have
|
||||
credentials. When you log in, the command stores encoded credentials in
|
||||
`$HOME/.docker/config.json` on Linux or `%USERPROFILE%/.docker/config.json` on Windows.
|
||||
credentials. When you log in, the command stores credentials in
|
||||
`$HOME/.docker/config.json` on Linux or `%USERPROFILE%/.docker/config.json` on
|
||||
Windows, via the procedure described below.
|
||||
|
||||
### Credentials store
|
||||
|
||||
@@ -82,6 +83,7 @@ you can download them from:
|
||||
- D-Bus Secret Service: https://github.com/docker/docker-credential-helpers/releases
|
||||
- Apple macOS keychain: https://github.com/docker/docker-credential-helpers/releases
|
||||
- Microsoft Windows Credential Manager: https://github.com/docker/docker-credential-helpers/releases
|
||||
- [pass](https://www.passwordstore.org/): https://github.com/docker/docker-credential-helpers/releases
|
||||
|
||||
You need to specify the credentials store in `$HOME/.docker/config.json`
|
||||
to tell the docker engine to use it. The value of the config property should be
|
||||
@@ -97,6 +99,15 @@ For example, to use `docker-credential-osxkeychain`:
|
||||
If you are currently logged in, run `docker logout` to remove
|
||||
the credentials from the file and run `docker login` again.
|
||||
|
||||
### Default behavior
|
||||
|
||||
By default, Docker looks for the native binary on each of the platforms, i.e.
|
||||
"osxkeychain" on macOS, "wincred" on windows, and "pass" on Linux. A special
|
||||
case is that on Linux, Docker will fall back to the "secretservice" binary if
|
||||
it cannot find the "pass" binary. If none of these binaries are present, it
|
||||
stores the credentials (i.e. password) in base64 encoding in the config files
|
||||
described above.
|
||||
|
||||
### Credential helper protocol
|
||||
|
||||
Credential helpers can be any program or script that follows a very simple protocol.
|
||||
|
||||
@@ -94,7 +94,10 @@ for example `--advertise-addr eth0:2377`.
|
||||
Specifying a port is optional. If the value is a bare IP address, or interface
|
||||
name, the default port 2377 will be used.
|
||||
|
||||
This flag is generally not necessary when joining an existing swarm.
|
||||
This flag is generally not necessary when joining an existing swarm. If
|
||||
you're joining new nodes through a load balancer, you should use this flag to
|
||||
ensure the node advertises its IP address and not the IP address of the load
|
||||
balancer.
|
||||
|
||||
### `--data-path-addr`
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
title: "trust revoke"
|
||||
description: "The revoke command description and usage"
|
||||
keywords: "revoke, 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 revoke
|
||||
|
||||
```markdown
|
||||
Usage: docker trust revoke [OPTIONS] IMAGE[:TAG]
|
||||
|
||||
Remove trust for an image
|
||||
|
||||
Options:
|
||||
--help Print usage
|
||||
-y, --yes Do not prompt for confirmation
|
||||
```
|
||||
|
||||
## Description
|
||||
|
||||
`docker trust revoke` removes signatures from tags in signed repositories.
|
||||
|
||||
`docker trust revoke` is currently experimental.
|
||||
|
||||
## Examples
|
||||
|
||||
### Revoke signatures from a signed tag
|
||||
|
||||
Here's an example of a repo with two signed tags:
|
||||
|
||||
|
||||
```bash
|
||||
$ docker trust view example/trust-demo
|
||||
SIGNED TAG DIGEST SIGNERS
|
||||
red 852cc04935f930a857b630edc4ed6131e91b22073bcc216698842e44f64d2943 alice
|
||||
blue f1c38dbaeeb473c36716f6494d803fbfbe9d8a76916f7c0093f227821e378197 alice, bob
|
||||
|
||||
List of signers and their keys for example/trust-demo:
|
||||
|
||||
SIGNER KEYS
|
||||
alice 05e87edcaecb
|
||||
bob 5600f5ab76a2
|
||||
|
||||
Administrative keys for example/trust-demo:
|
||||
Repository Key: ecc457614c9fc399da523a5f4e24fe306a0a6ee1cc79a10e4555b3c6ab02f71e
|
||||
Root Key: 3cb2228f6561e58f46dbc4cda4fcaff9d5ef22e865a94636f82450d1d2234949
|
||||
```
|
||||
|
||||
When `alice`, one of the signers, runs `docker trust revoke`:
|
||||
|
||||
```bash
|
||||
$ docker trust revoke example/trust-demo:red
|
||||
Enter passphrase for delegation key with ID 27d42a8:
|
||||
Successfully deleted signature for example/trust-demo:red
|
||||
```
|
||||
|
||||
After revocation, the tag is removed from the list of released tags:
|
||||
|
||||
```bash
|
||||
$ docker trust view example/trust-demo
|
||||
SIGNED TAG DIGEST SIGNERS
|
||||
blue f1c38dbaeeb473c36716f6494d803fbfbe9d8a76916f7c0093f227821e378197 alice, bob
|
||||
|
||||
List of signers and their keys for example/trust-demo:
|
||||
|
||||
SIGNER KEYS
|
||||
alice 05e87edcaecb
|
||||
bob 5600f5ab76a2
|
||||
|
||||
Administrative keys for example/trust-demo:
|
||||
Repository Key: ecc457614c9fc399da523a5f4e24fe306a0a6ee1cc79a10e4555b3c6ab02f71e
|
||||
Root Key: 3cb2228f6561e58f46dbc4cda4fcaff9d5ef22e865a94636f82450d1d2234949
|
||||
```
|
||||
|
||||
### Revoke signatures on all tags in a repository
|
||||
|
||||
When no tag is specified, `docker trust` revokes all signatures that you have a signing key for.
|
||||
|
||||
```bash
|
||||
$ docker trust view example/trust-demo
|
||||
SIGNED TAG DIGEST SIGNERS
|
||||
red 852cc04935f930a857b630edc4ed6131e91b22073bcc216698842e44f64d2943 alice
|
||||
blue f1c38dbaeeb473c36716f6494d803fbfbe9d8a76916f7c0093f227821e378197 alice, bob
|
||||
|
||||
List of signers and their keys for example/trust-demo:
|
||||
|
||||
SIGNER KEYS
|
||||
alice 05e87edcaecb
|
||||
bob 5600f5ab76a2
|
||||
|
||||
Administrative keys for example/trust-demo:
|
||||
Repository Key: ecc457614c9fc399da523a5f4e24fe306a0a6ee1cc79a10e4555b3c6ab02f71e
|
||||
Root Key: 3cb2228f6561e58f46dbc4cda4fcaff9d5ef22e865a94636f82450d1d2234949
|
||||
```
|
||||
|
||||
When `alice`, one of the signers, runs `docker trust revoke`:
|
||||
|
||||
```bash
|
||||
$ docker trust revoke example/trust-demo
|
||||
Please confirm you would like to delete all signature data for example/trust-demo? [y/N] y
|
||||
Enter passphrase for delegation key with ID 27d42a8:
|
||||
Successfully deleted signature for example/trust-demo
|
||||
```
|
||||
|
||||
All tags that have `alice`'s signature on them are removed from the list of released tags:
|
||||
|
||||
```bash
|
||||
$ docker trust view example/trust-demo
|
||||
|
||||
No signatures for example/trust-demo
|
||||
|
||||
|
||||
List of signers and their keys for example/trust-demo:
|
||||
|
||||
SIGNER KEYS
|
||||
alice 05e87edcaecb
|
||||
bob 5600f5ab76a2
|
||||
|
||||
Administrative keys for example/trust-demo:
|
||||
Repository Key: ecc457614c9fc399da523a5f4e24fe306a0a6ee1cc79a10e4555b3c6ab02f71e
|
||||
Root Key: 3cb2228f6561e58f46dbc4cda4fcaff9d5ef22e865a94636f82450d1d2234949
|
||||
```
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
---
|
||||
title: "trust sign"
|
||||
description: "The sign command description and usage"
|
||||
keywords: "sign, 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 sign
|
||||
|
||||
```markdown
|
||||
Usage: docker trust sign [OPTIONS] IMAGE:TAG
|
||||
|
||||
Sign an image
|
||||
|
||||
```
|
||||
|
||||
## Description
|
||||
|
||||
`docker trust sign` adds signatures to tags to create signed repositories.
|
||||
|
||||
`docker trust sign` is currently experimental.
|
||||
|
||||
## Examples
|
||||
|
||||
### Sign a tag as a repo admin
|
||||
|
||||
Given an image:
|
||||
|
||||
```bash
|
||||
$ docker trust view example/trust-demo
|
||||
SIGNED TAG DIGEST SIGNERS
|
||||
v1 c24134c079c35e698060beabe110bb83ab285d0d978de7d92fed2c8c83570a41 (Repo Admin)
|
||||
|
||||
Administrative keys for example/trust-demo:
|
||||
Repository Key: 36d4c3601102fa7c5712a343c03b94469e5835fb27c191b529c06fd19c14a942
|
||||
Root Key: 246d360f7c53a9021ee7d4259e3c5692f3f1f7ad4737b1ea8c7b8da741ad980b
|
||||
```
|
||||
|
||||
Sign a new tag with `docker trust sign`:
|
||||
|
||||
```bash
|
||||
$ docker trust sign example/trust-demo:v2
|
||||
Signing and pushing trust metadata for example/trust-demo:v2
|
||||
The push refers to a repository [docker.io/example/trust-demo]
|
||||
eed4e566104a: Layer already exists
|
||||
77edfb6d1e3c: Layer already exists
|
||||
c69f806905c2: Layer already exists
|
||||
582f327616f1: Layer already exists
|
||||
a3fbb648f0bd: Layer already exists
|
||||
5eac2de68a97: Layer already exists
|
||||
8d4d1ab5ff74: Layer already exists
|
||||
v2: digest: sha256:8f6f460abf0436922df7eb06d28b3cdf733d2cac1a185456c26debbff0839c56 size: 1787
|
||||
Signing and pushing trust metadata
|
||||
Enter passphrase for repository key with ID 36d4c36:
|
||||
Successfully signed "docker.io/example/trust-demo":v2
|
||||
```
|
||||
|
||||
`docker trust view` lists the new signature:
|
||||
|
||||
```bash
|
||||
$ docker trust view example/trust-demo
|
||||
SIGNED TAG DIGEST SIGNERS
|
||||
v1 c24134c079c35e698060beabe110bb83ab285d0d978de7d92fed2c8c83570a41 (Repo Admin)
|
||||
v2 8f6f460abf0436922df7eb06d28b3cdf733d2cac1a185456c26debbff0839c56 (Repo Admin)
|
||||
|
||||
Administrative keys for example/trust-demo:
|
||||
Repository Key: 36d4c3601102fa7c5712a343c03b94469e5835fb27c191b529c06fd19c14a942
|
||||
Root Key: 246d360f7c53a9021ee7d4259e3c5692f3f1f7ad4737b1ea8c7b8da741ad980b
|
||||
```
|
||||
|
||||
### Sign a tag as a signer
|
||||
|
||||
Given an image:
|
||||
|
||||
```bash
|
||||
$ docker trust view example/trust-demo
|
||||
|
||||
No signatures for example/trust-demo
|
||||
|
||||
|
||||
List of signers and their keys for example/trust-demo:
|
||||
|
||||
SIGNER KEYS
|
||||
alice 05e87edcaecb
|
||||
bob 5600f5ab76a2
|
||||
|
||||
Administrative keys for example/trust-demo:
|
||||
Repository Key: ecc457614c9fc399da523a5f4e24fe306a0a6ee1cc79a10e4555b3c6ab02f71e
|
||||
Root Key: 3cb2228f6561e58f46dbc4cda4fcaff9d5ef22e865a94636f82450d1d2234949
|
||||
```
|
||||
|
||||
Sign a new tag with `docker trust sign`:
|
||||
|
||||
```bash
|
||||
$ docker trust sign example/trust-demo:v1
|
||||
Signing and pushing trust metadata for example/trust-demo:v1
|
||||
The push refers to a repository [docker.io/example/trust-demo]
|
||||
26b126eb8632: Layer already exists
|
||||
220d34b5f6c9: Layer already exists
|
||||
8a5132998025: Layer already exists
|
||||
aca233ed29c3: Layer already exists
|
||||
e5d2f035d7a4: Layer already exists
|
||||
v1: digest: sha256:74d4bfa917d55d53c7df3d2ab20a8d926874d61c3da5ef6de15dd2654fc467c4 size: 1357
|
||||
Signing and pushing trust metadata
|
||||
Enter passphrase for delegation key with ID 27d42a8:
|
||||
Successfully signed "docker.io/example/trust-demo":v1
|
||||
```
|
||||
|
||||
`docker trust view` lists the new signature:
|
||||
|
||||
```bash
|
||||
$ docker trust view example/trust-demo
|
||||
SIGNED TAG DIGEST SIGNERS
|
||||
v1 74d4bfa917d55d53c7df3d2ab20a8d926874d61c3da5ef6de15dd2654fc467c4 alice
|
||||
|
||||
List of signers and their keys for example/trust-demo:
|
||||
|
||||
SIGNER KEYS
|
||||
alice 05e87edcaecb
|
||||
bob 5600f5ab76a2
|
||||
|
||||
Administrative keys for example/trust-demo:
|
||||
Repository Key: ecc457614c9fc399da523a5f4e24fe306a0a6ee1cc79a10e4555b3c6ab02f71e
|
||||
Root Key: 3cb2228f6561e58f46dbc4cda4fcaff9d5ef22e865a94636f82450d1d2234949
|
||||
```
|
||||
|
||||
## Initialize a new repo and sign a tag
|
||||
|
||||
When signing an image on a repo for the first time, `docker trust sign` sets up new keys before signing the image.
|
||||
|
||||
```bash
|
||||
$ docker trust view example/trust-demo
|
||||
No signatures or cannot access example/trust-demo
|
||||
```
|
||||
|
||||
```bash
|
||||
$ docker trust sign example/trust-demo:v1
|
||||
Signing and pushing trust metadata for example/trust-demo:v1
|
||||
Enter passphrase for root key with ID 36cac18:
|
||||
Enter passphrase for new repository key with ID 731396b:
|
||||
Repeat passphrase for new repository key with ID 731396b:
|
||||
Enter passphrase for new alice key with ID 6d52b29:
|
||||
Repeat passphrase for new alice key with ID 6d52b29:
|
||||
Created signer: alice
|
||||
Finished initializing "docker.io/example/trust-demo"
|
||||
The push refers to a repository [docker.io/example/trust-demo]
|
||||
eed4e566104a: Layer already exists
|
||||
77edfb6d1e3c: Layer already exists
|
||||
c69f806905c2: Layer already exists
|
||||
582f327616f1: Layer already exists
|
||||
a3fbb648f0bd: Layer already exists
|
||||
5eac2de68a97: Layer already exists
|
||||
8d4d1ab5ff74: Layer already exists
|
||||
v1: digest: sha256:8f6f460abf0436922df7eb06d28b3cdf733d2cac1a185456c26debbff0839c56 size: 1787
|
||||
Signing and pushing trust metadata
|
||||
Enter passphrase for alice key with ID 6d52b29:
|
||||
Successfully signed "docker.io/example/trust-demo":v1
|
||||
```
|
||||
|
||||
```bash
|
||||
$ docker trust view example/trust-demo
|
||||
SIGNED TAG DIGEST SIGNERS
|
||||
v1 8f6f460abf0436922df7eb06d28b3cdf733d2cac1a185456c26debbff0839c56 alice
|
||||
|
||||
List of signers and their keys for example/trust-demo:
|
||||
|
||||
SIGNER KEYS
|
||||
alice 6d52b29d940f
|
||||
|
||||
Administrative keys for example/trust-demo:
|
||||
Repository Key: 731396b65eac3ef5ec01406801bdfb70feb40c17808d2222427c18046eb63beb
|
||||
Root Key: 70d174714bd1461f6c58cb3ef39087c8fdc7633bb11a98af844fd9a04e208103
|
||||
```
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
title: "trust view"
|
||||
description: "The view command description and usage"
|
||||
keywords: "view, 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 view
|
||||
|
||||
```markdown
|
||||
Usage: docker trust view [OPTIONS] IMAGE[:TAG]
|
||||
|
||||
Display detailed information about keys and signatures
|
||||
|
||||
```
|
||||
|
||||
## Description
|
||||
|
||||
`docker trust view` provides detailed information on signed repositories.
|
||||
This includes all image tags that are signed, who signed them, and who can sign
|
||||
new tags.
|
||||
|
||||
By default, `docker trust view` renders results in a table.
|
||||
|
||||
`docker trust view` is currently experimental.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
### Get details about signatures for a single image tag
|
||||
|
||||
|
||||
```bash
|
||||
$ docker trust view alpine:latest
|
||||
|
||||
SIGNED TAG DIGEST SIGNERS
|
||||
latest 1072e499f3f655a032e88542330cf75b02e7bdf673278f701d7ba61629ee3ebe (Repo Admin)
|
||||
|
||||
Administrative keys for alpine:latest:
|
||||
Repository Key: 5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd
|
||||
Root Key: a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce
|
||||
```
|
||||
|
||||
The `SIGNED TAG` is the signed image tag with a unique content-addressable `DIGEST`. `SIGNERS` lists all entities who have signed.
|
||||
|
||||
The administrative keys listed specify the root key of trust, as well as the administrative repository key. These keys are responsible for modifying signers, and rotating keys for the signed repository.
|
||||
|
||||
If signers are set up for the repository via other `docker trust` commands, `docker trust view` displays them appropriately as a `SIGNER` and specify their `KEYS`:
|
||||
|
||||
```bash
|
||||
$ docker trust view my-image:purple
|
||||
SIGNED TAG DIGEST SIGNERS
|
||||
purple 941d3dba358621ce3c41ef67b47cf80f701ff80cdf46b5cc86587eaebfe45557 alice, bob, carol
|
||||
|
||||
List of signers and their keys:
|
||||
|
||||
SIGNER KEYS
|
||||
alice 47caae5b3e61, a85aab9d20a4
|
||||
bob 034370bcbd77, 82a66673242c
|
||||
carol b6f9f8e1aab0
|
||||
|
||||
Administrative keys for my-image:
|
||||
Repository Key: 27df2c8187e7543345c2e0bf3a1262e0bc63a72754e9a7395eac3f747ec23a44
|
||||
Root Key: 40b66ccc8b176be8c7d365a17f3e046d1c3494e053dd57cfeacfe2e19c4f8e8f
|
||||
```
|
||||
|
||||
If the image tag is unsigned or unavailable, `docker trust view` does not display any signed tags.
|
||||
|
||||
```bash
|
||||
$ docker trust view unsigned-img
|
||||
No signatures or cannot access unsigned-img
|
||||
```
|
||||
|
||||
However, if other tags are signed in the same image repository, `docker trust view` reports relevant key information.
|
||||
|
||||
```bash
|
||||
$ docker trust view alpine:unsigned
|
||||
|
||||
No signatures for alpine:unsigned
|
||||
|
||||
|
||||
Administrative keys for alpine:unsigned:
|
||||
Repository Key: 5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd
|
||||
Root Key: a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce
|
||||
```
|
||||
|
||||
### Get details about signatures for all image tags in a repository
|
||||
|
||||
```bash
|
||||
$ docker trust view alpine
|
||||
SIGNED TAG DIGEST SIGNERS
|
||||
2.6 9ace551613070689a12857d62c30ef0daa9a376107ec0fff0e34786cedb3399b (Repo Admin)
|
||||
2.7 9f08005dff552038f0ad2f46b8e65ff3d25641747d3912e3ea8da6785046561a (Repo Admin)
|
||||
3.1 d9477888b78e8c6392e0be8b2e73f8c67e2894ff9d4b8e467d1488fcceec21c8 (Repo Admin)
|
||||
3.2 19826d59171c2eb7e90ce52bfd822993bef6a6fe3ae6bb4a49f8c1d0a01e99c7 (Repo Admin)
|
||||
3.3 8fd4b76819e1e5baac82bd0a3d03abfe3906e034cc5ee32100d12aaaf3956dc7 (Repo Admin)
|
||||
3.4 833ad81ace8277324f3ca8c91c02bdcf1d13988d8ecf8a3f97ecdd69d0390ce9 (Repo Admin)
|
||||
3.5 af2a5bd2f8de8fc1ecabf1c76611cdc6a5f1ada1a2bdd7d3816e121b70300308 (Repo Admin)
|
||||
3.6 1072e499f3f655a032e88542330cf75b02e7bdf673278f701d7ba61629ee3ebe (Repo Admin)
|
||||
edge 79d50d15bd7ea48ea00cf3dd343b0e740c1afaa8e899bee475236ef338e1b53b (Repo Admin)
|
||||
latest 1072e499f3f655a032e88542330cf75b02e7bdf673278f701d7ba61629ee3ebe (Repo Admin)
|
||||
|
||||
Administrative keys for alpine:
|
||||
Repository Key: 5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd
|
||||
Root Key: a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce
|
||||
```
|
||||
|
||||
Here's an example with signers that are set up by `docker trust` commands:
|
||||
|
||||
```bash
|
||||
$ docker trust view my-image
|
||||
SIGNED TAG DIGEST SIGNERS
|
||||
red 852cc04935f930a857b630edc4ed6131e91b22073bcc216698842e44f64d2943 alice
|
||||
blue f1c38dbaeeb473c36716f6494d803fbfbe9d8a76916f7c0093f227821e378197 alice, bob
|
||||
green cae8fedc840f90c8057e1c24637d11865743ab1e61a972c1c9da06ec2de9a139 alice, bob
|
||||
yellow 9cc65fc3126790e683d1b92f307a71f48f75fa7dd47a7b03145a123eaf0b45ba carol
|
||||
purple 941d3dba358621ce3c41ef67b47cf80f701ff80cdf46b5cc86587eaebfe45557 alice, bob, carol
|
||||
orange d6c271baa6d271bcc24ef1cbd65abf39123c17d2e83455bdab545a1a9093fc1c alice
|
||||
|
||||
List of signers and their keys for my-image:
|
||||
|
||||
SIGNER KEYS
|
||||
alice 47caae5b3e61, a85aab9d20a4
|
||||
bob 034370bcbd77, 82a66673242c
|
||||
carol b6f9f8e1aab0
|
||||
|
||||
Administrative keys for my-image:
|
||||
Repository Key: 27df2c8187e7543345c2e0bf3a1262e0bc63a72754e9a7395eac3f747ec23a44
|
||||
Root Key: 40b66ccc8b176be8c7d365a17f3e046d1c3494e053dd57cfeacfe2e19c4f8e8f
|
||||
```
|
||||
@@ -2,25 +2,31 @@ package test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/docker/client"
|
||||
notaryclient "github.com/docker/notary/client"
|
||||
)
|
||||
|
||||
type notaryClientFuncType func(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
||||
|
||||
// FakeCli emulates the default DockerCli
|
||||
type FakeCli struct {
|
||||
command.DockerCli
|
||||
client client.APIClient
|
||||
configfile *configfile.ConfigFile
|
||||
out *command.OutStream
|
||||
outBuffer *bytes.Buffer
|
||||
err *bytes.Buffer
|
||||
in *command.InStream
|
||||
server command.ServerInfo
|
||||
client client.APIClient
|
||||
configfile *configfile.ConfigFile
|
||||
out *command.OutStream
|
||||
outBuffer *bytes.Buffer
|
||||
err *bytes.Buffer
|
||||
in *command.InStream
|
||||
server command.ServerInfo
|
||||
notaryClientFunc notaryClientFuncType
|
||||
}
|
||||
|
||||
// NewFakeCli returns a fake for the command.Cli interface
|
||||
@@ -91,3 +97,16 @@ func (c *FakeCli) OutBuffer() *bytes.Buffer {
|
||||
func (c *FakeCli) ErrBuffer() *bytes.Buffer {
|
||||
return c.err
|
||||
}
|
||||
|
||||
// SetNotaryClient sets the internal getter for retrieving a NotaryClient
|
||||
func (c *FakeCli) SetNotaryClient(notaryClientFunc notaryClientFuncType) {
|
||||
c.notaryClientFunc = notaryClientFunc
|
||||
}
|
||||
|
||||
// NotaryClient returns an err for testing unless defined
|
||||
func (c *FakeCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) {
|
||||
if c.notaryClientFunc != nil {
|
||||
return c.notaryClientFunc(imgRefAndAuth, actions)
|
||||
}
|
||||
return nil, fmt.Errorf("no notary client available unless defined")
|
||||
}
|
||||
|
||||
@@ -84,4 +84,5 @@ change propagation properties of source mount. Say `/` is source mount for
|
||||
|
||||
|
||||
To disable automatic copying of data from the container path to the volume, use
|
||||
the `nocopy` flag. The `nocopy` flag can be set on bind mounts and named volumes.
|
||||
the `nocopy` flag. The `nocopy` flag can be set on named volumes, and does not
|
||||
apply to bind mounts..
|
||||
|
||||
@@ -5,7 +5,7 @@ github.com/cpuguy83/go-md2man a65d4d2de4d5f7c74868dfa9b202a3c8be315aaa
|
||||
github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76
|
||||
github.com/docker/distribution edc3ab29cdff8694dd6feb85cfeb4b5f1b38ed9c
|
||||
github.com/docker/docker 84144a8c66c1bb2af8fa997288f51ef2719971b4
|
||||
github.com/docker/docker-credential-helpers v0.5.1
|
||||
github.com/docker/docker-credential-helpers 3c90bd29a46b943b2a9842987b58fb91a7c1819b
|
||||
|
||||
# the docker/go package contains a customized version of canonical/json
|
||||
# and is used by Notary. The package is periodically rebased on current Go versions.
|
||||
@@ -13,8 +13,8 @@ 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 v0.4.2-sirupsen https://github.com/simonferquel/notary.git
|
||||
github.com/docker/swarmkit 0554c9bc9a485025e89b8e5c2c1f0d75961906a2
|
||||
github.com/docker/notary 8a1de3cfc3f1408e54d6364fc949214a4883a9f3
|
||||
github.com/docker/swarmkit 79381d0840be27f8b3f5c667b348a4467d866eeb
|
||||
github.com/flynn-archive/go-shlex 3f9db97f856818214da2e1057f8ad84803971cff
|
||||
github.com/gogo/protobuf v0.4
|
||||
github.com/golang/protobuf 7a211bcf3bce0e3f1d74f9894916e6f116ae83b4
|
||||
|
||||
Generated
Vendored
+17
-5
@@ -1,6 +1,7 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -17,15 +18,26 @@ type ProgramFunc func(args ...string) Program
|
||||
|
||||
// NewShellProgramFunc creates programs that are executed in a Shell.
|
||||
func NewShellProgramFunc(name string) ProgramFunc {
|
||||
return NewShellProgramFuncWithEnv(name, nil)
|
||||
}
|
||||
|
||||
// NewShellProgramFuncWithEnv creates programs that are executed in a Shell with environment variables
|
||||
func NewShellProgramFuncWithEnv(name string, env *map[string]string) ProgramFunc {
|
||||
return func(args ...string) Program {
|
||||
return &Shell{cmd: newCmdRedirectErr(name, args)}
|
||||
return &Shell{cmd: createProgramCmdRedirectErr(name, args, env)}
|
||||
}
|
||||
}
|
||||
|
||||
func newCmdRedirectErr(name string, args []string) *exec.Cmd {
|
||||
newCmd := exec.Command(name, args...)
|
||||
newCmd.Stderr = os.Stderr
|
||||
return newCmd
|
||||
func createProgramCmdRedirectErr(commandName string, args []string, env *map[string]string) *exec.Cmd {
|
||||
programCmd := exec.Command(commandName, args...)
|
||||
programCmd.Env = os.Environ()
|
||||
if env != nil {
|
||||
for k, v := range *env {
|
||||
programCmd.Env = append(programCmd.Env, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
}
|
||||
programCmd.Stderr = os.Stderr
|
||||
return programCmd
|
||||
}
|
||||
|
||||
// Shell invokes shell commands to talk with a remote credentials helper.
|
||||
|
||||
Generated
Vendored
+13
-4
@@ -33,11 +33,12 @@ func (c *Credentials) isValid() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Docker credentials should be labeled as such in credentials stores that allow labelling.
|
||||
// CredsLabel holds the way Docker credentials should be labeled as such in credentials stores that allow labelling.
|
||||
// That label allows to filter out non-Docker credentials too at lookup/search in macOS keychain,
|
||||
// Windows credentials manager and Linux libsecret. Default value is "Docker Credentials"
|
||||
var CredsLabel = "Docker Credentials"
|
||||
|
||||
// SetCredsLabel is a simple setter for CredsLabel
|
||||
func SetCredsLabel(label string) {
|
||||
CredsLabel = label
|
||||
}
|
||||
@@ -50,7 +51,7 @@ func SetCredsLabel(label string) {
|
||||
func Serve(helper Helper) {
|
||||
var err error
|
||||
if len(os.Args) != 2 {
|
||||
err = fmt.Errorf("Usage: %s <store|get|erase|list>", os.Args[0])
|
||||
err = fmt.Errorf("Usage: %s <store|get|erase|list|version>", os.Args[0])
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
@@ -74,6 +75,8 @@ func HandleCommand(helper Helper, key string, in io.Reader, out io.Writer) error
|
||||
return Erase(helper, in)
|
||||
case "list":
|
||||
return List(helper, out)
|
||||
case "version":
|
||||
return PrintVersion(out)
|
||||
}
|
||||
return fmt.Errorf("Unknown credential action `%s`", key)
|
||||
}
|
||||
@@ -131,8 +134,8 @@ func Get(helper Helper, reader io.Reader, writer io.Writer) error {
|
||||
|
||||
resp := Credentials{
|
||||
ServerURL: serverURL,
|
||||
Username: username,
|
||||
Secret: secret,
|
||||
Username: username,
|
||||
Secret: secret,
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
@@ -175,3 +178,9 @@ func List(helper Helper, writer io.Writer) error {
|
||||
}
|
||||
return json.NewEncoder(writer).Encode(accts)
|
||||
}
|
||||
|
||||
//PrintVersion outputs the current version.
|
||||
func PrintVersion(writer io.Writer) error {
|
||||
fmt.Fprintln(writer, Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
Generated
Vendored
+1
-4
@@ -8,7 +8,7 @@ const (
|
||||
// ErrCredentialsMissingServerURL and ErrCredentialsMissingUsername standardize
|
||||
// invalid credentials or credentials management operations
|
||||
errCredentialsMissingServerURLMessage = "no credentials server URL"
|
||||
errCredentialsMissingUsernameMessage = "no credentials username"
|
||||
errCredentialsMissingUsernameMessage = "no credentials username"
|
||||
)
|
||||
|
||||
// errCredentialsNotFound represents an error
|
||||
@@ -43,7 +43,6 @@ func IsErrCredentialsNotFoundMessage(err string) bool {
|
||||
return err == errCredentialsNotFoundMessage
|
||||
}
|
||||
|
||||
|
||||
// errCredentialsMissingServerURL represents an error raised
|
||||
// when the credentials object has no server URL or when no
|
||||
// server URL is provided to a credentials operation requiring
|
||||
@@ -64,7 +63,6 @@ func (errCredentialsMissingUsername) Error() string {
|
||||
return errCredentialsMissingUsernameMessage
|
||||
}
|
||||
|
||||
|
||||
// NewErrCredentialsMissingServerURL creates a new error for
|
||||
// errCredentialsMissingServerURL.
|
||||
func NewErrCredentialsMissingServerURL() error {
|
||||
@@ -77,7 +75,6 @@ func NewErrCredentialsMissingUsername() error {
|
||||
return errCredentialsMissingUsername{}
|
||||
}
|
||||
|
||||
|
||||
// IsCredentialsMissingServerURL returns true if the error
|
||||
// was an errCredentialsMissingServerURL.
|
||||
func IsCredentialsMissingServerURL(err error) bool {
|
||||
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
package credentials
|
||||
|
||||
// Version holds a string describing the current version
|
||||
const Version = "0.5.2"
|
||||
Generated
Vendored
+38
-12
@@ -135,30 +135,27 @@ func (h Osxkeychain) List() (map[string]string, error) {
|
||||
}
|
||||
|
||||
func splitServer(serverURL string) (*C.struct_Server, error) {
|
||||
u, err := url.Parse(serverURL)
|
||||
u, err := parseURL(serverURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hostAndPort := strings.Split(u.Host, ":")
|
||||
host := hostAndPort[0]
|
||||
proto := C.kSecProtocolTypeHTTPS
|
||||
if u.Scheme == "http" {
|
||||
proto = C.kSecProtocolTypeHTTP
|
||||
}
|
||||
var port int
|
||||
if len(hostAndPort) == 2 {
|
||||
p, err := strconv.Atoi(hostAndPort[1])
|
||||
p := getPort(u)
|
||||
if p != "" {
|
||||
port, err = strconv.Atoi(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
port = p
|
||||
}
|
||||
|
||||
proto := C.kSecProtocolTypeHTTPS
|
||||
if u.Scheme != "https" {
|
||||
proto = C.kSecProtocolTypeHTTP
|
||||
}
|
||||
|
||||
return &C.struct_Server{
|
||||
proto: C.SecProtocolType(proto),
|
||||
host: C.CString(host),
|
||||
host: C.CString(getHostname(u)),
|
||||
port: C.uint(port),
|
||||
path: C.CString(u.Path),
|
||||
}, nil
|
||||
@@ -168,3 +165,32 @@ func freeServer(s *C.struct_Server) {
|
||||
C.free(unsafe.Pointer(s.host))
|
||||
C.free(unsafe.Pointer(s.path))
|
||||
}
|
||||
|
||||
// parseURL parses and validates a given serverURL to an url.URL, and
|
||||
// returns an error if validation failed. Querystring parameters are
|
||||
// omitted in the resulting URL, because they are not used in the helper.
|
||||
//
|
||||
// If serverURL does not have a valid scheme, `//` is used as scheme
|
||||
// before parsing. This prevents the hostname being used as path,
|
||||
// and the credentials being stored without host.
|
||||
func parseURL(serverURL string) (*url.URL, error) {
|
||||
// Check if serverURL has a scheme, otherwise add `//` as scheme.
|
||||
if !strings.Contains(serverURL, "://") && !strings.HasPrefix(serverURL, "//") {
|
||||
serverURL = "//" + serverURL
|
||||
}
|
||||
|
||||
u, err := url.Parse(serverURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.Scheme != "" && u.Scheme != "https" && u.Scheme != "http" {
|
||||
return nil, errors.New("unsupported scheme: " + u.Scheme)
|
||||
}
|
||||
if getHostname(u) == "" {
|
||||
return nil, errors.New("no hostname in URL")
|
||||
}
|
||||
|
||||
u.RawQuery = ""
|
||||
return u, nil
|
||||
}
|
||||
|
||||
Generated
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
//+build go1.8
|
||||
|
||||
package osxkeychain
|
||||
|
||||
import "net/url"
|
||||
|
||||
func getHostname(u *url.URL) string {
|
||||
return u.Hostname()
|
||||
}
|
||||
|
||||
func getPort(u *url.URL) string {
|
||||
return u.Port()
|
||||
}
|
||||
Generated
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
//+build !go1.8
|
||||
|
||||
package osxkeychain
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getHostname(u *url.URL) string {
|
||||
return stripPort(u.Host)
|
||||
}
|
||||
|
||||
func getPort(u *url.URL) string {
|
||||
return portOnly(u.Host)
|
||||
}
|
||||
|
||||
func stripPort(hostport string) string {
|
||||
colon := strings.IndexByte(hostport, ':')
|
||||
if colon == -1 {
|
||||
return hostport
|
||||
}
|
||||
if i := strings.IndexByte(hostport, ']'); i != -1 {
|
||||
return strings.TrimPrefix(hostport[:i], "[")
|
||||
}
|
||||
return hostport[:colon]
|
||||
}
|
||||
|
||||
func portOnly(hostport string) string {
|
||||
colon := strings.IndexByte(hostport, ':')
|
||||
if colon == -1 {
|
||||
return ""
|
||||
}
|
||||
if i := strings.Index(hostport, "]:"); i != -1 {
|
||||
return hostport[i+len("]:"):]
|
||||
}
|
||||
if strings.Contains(hostport, "]") {
|
||||
return ""
|
||||
}
|
||||
return hostport[colon+len(":"):]
|
||||
}
|
||||
Generated
Vendored
+208
@@ -0,0 +1,208 @@
|
||||
// A `pass` based credential helper. Passwords are stored as arguments to pass
|
||||
// of the form: "$PASS_FOLDER/base64-url(serverURL)/username". We base64-url
|
||||
// encode the serverURL, because under the hood pass uses files and folders, so
|
||||
// /s will get translated into additional folders.
|
||||
package pass
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker-credential-helpers/credentials"
|
||||
)
|
||||
|
||||
const PASS_FOLDER = "docker-credential-helpers"
|
||||
|
||||
var (
|
||||
PassInitialized bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
PassInitialized = exec.Command("pass").Run() == nil
|
||||
}
|
||||
|
||||
func runPass(stdinContent string, args ...string) (string, error) {
|
||||
cmd := exec.Command("pass", args...)
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer stdin.Close()
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer stderr.Close()
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer stdout.Close()
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = stdin.Write([]byte(stdinContent))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
stdin.Close()
|
||||
|
||||
errContent, err := ioutil.ReadAll(stderr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading stderr: %s", err)
|
||||
}
|
||||
|
||||
result, err := ioutil.ReadAll(stdout)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error reading stdout: %s", err)
|
||||
}
|
||||
|
||||
cmdErr := cmd.Wait()
|
||||
if cmdErr != nil {
|
||||
return "", fmt.Errorf("%s: %s", cmdErr, errContent)
|
||||
}
|
||||
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
// Pass handles secrets using Linux secret-service as a store.
|
||||
type Pass struct{}
|
||||
|
||||
// Add adds new credentials to the keychain.
|
||||
func (h Pass) Add(creds *credentials.Credentials) error {
|
||||
if !PassInitialized {
|
||||
return errors.New("pass store is uninitialized")
|
||||
}
|
||||
|
||||
if creds == nil {
|
||||
return errors.New("missing credentials")
|
||||
}
|
||||
|
||||
encoded := base64.URLEncoding.EncodeToString([]byte(creds.ServerURL))
|
||||
|
||||
_, err := runPass(creds.Secret, "insert", "-f", "-m", path.Join(PASS_FOLDER, encoded, creds.Username))
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete removes credentials from the store.
|
||||
func (h Pass) Delete(serverURL string) error {
|
||||
if !PassInitialized {
|
||||
return errors.New("pass store is uninitialized")
|
||||
}
|
||||
|
||||
if serverURL == "" {
|
||||
return errors.New("missing server url")
|
||||
}
|
||||
|
||||
encoded := base64.URLEncoding.EncodeToString([]byte(serverURL))
|
||||
_, err := runPass("", "rm", "-rf", path.Join(PASS_FOLDER, encoded))
|
||||
return err
|
||||
}
|
||||
|
||||
// listPassDir lists all the contents of a directory in the password store.
|
||||
// Pass uses fancy unicode to emit stuff to stdout, so rather than try
|
||||
// and parse this, let's just look at the directory structure instead.
|
||||
func listPassDir(args ...string) ([]os.FileInfo, error) {
|
||||
passDir := os.ExpandEnv("$HOME/.password-store")
|
||||
for _, e := range os.Environ() {
|
||||
parts := strings.SplitN(e, "=", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
if parts[0] != "PASSWORD_STORE_DIR" {
|
||||
continue
|
||||
}
|
||||
|
||||
passDir = parts[1]
|
||||
break
|
||||
}
|
||||
|
||||
p := path.Join(append([]string{passDir, PASS_FOLDER}, args...)...)
|
||||
contents, err := ioutil.ReadDir(p)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []os.FileInfo{}, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
// Get returns the username and secret to use for a given registry server URL.
|
||||
func (h Pass) Get(serverURL string) (string, string, error) {
|
||||
if !PassInitialized {
|
||||
return "", "", errors.New("pass store is uninitialized")
|
||||
}
|
||||
|
||||
if serverURL == "" {
|
||||
return "", "", errors.New("missing server url")
|
||||
}
|
||||
|
||||
encoded := base64.URLEncoding.EncodeToString([]byte(serverURL))
|
||||
|
||||
usernames, err := listPassDir(encoded)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if len(usernames) < 1 {
|
||||
return "", "", fmt.Errorf("no usernames for %s", serverURL)
|
||||
}
|
||||
|
||||
actual := strings.TrimSuffix(usernames[0].Name(), ".gpg")
|
||||
secret, err := runPass("", "show", path.Join(PASS_FOLDER, encoded, actual))
|
||||
return actual, secret, err
|
||||
}
|
||||
|
||||
// List returns the stored URLs and corresponding usernames for a given credentials label
|
||||
func (h Pass) List() (map[string]string, error) {
|
||||
if !PassInitialized {
|
||||
return nil, errors.New("pass store is uninitialized")
|
||||
}
|
||||
|
||||
servers, err := listPassDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := map[string]string{}
|
||||
|
||||
for _, server := range servers {
|
||||
if !server.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
serverURL, err := base64.URLEncoding.DecodeString(server.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
usernames, err := listPassDir(server.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(usernames) < 1 {
|
||||
return nil, fmt.Errorf("no usernames for %s", serverURL)
|
||||
}
|
||||
|
||||
resp[string(serverURL)] = strings.TrimSuffix(usernames[0].Name(), ".gpg")
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
Generated
Vendored
+5
-2
@@ -105,8 +105,11 @@ func (h Secretservice) List() (map[string]string, error) {
|
||||
if listLen == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
pathTmp := (*[1 << 30]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen]
|
||||
acctTmp := (*[1 << 30]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen]
|
||||
// The maximum capacity of the following two slices is limited to (2^29)-1 to remain compatible
|
||||
// with 32-bit platforms. The size of a `*C.char` (a pointer) is 4 Byte on a 32-bit system
|
||||
// and (2^29)*4 == math.MaxInt32 + 1. -- See issue golang/go#13656
|
||||
pathTmp := (*[(1 << 29) - 1]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen]
|
||||
acctTmp := (*[(1 << 29) - 1]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen]
|
||||
for i := 0; i < listLen; i++ {
|
||||
resp[C.GoString(pathTmp[i])] = C.GoString(acctTmp[i])
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
Apache License
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
|
||||
+9
-4
@@ -78,18 +78,21 @@ to use `notary` with Docker images.
|
||||
|
||||
## Building Notary
|
||||
|
||||
Note that our [latest stable release](https://github.com/docker/notary/releases) is at the head of the
|
||||
[releases branch](https://github.com/docker/notary/tree/releases). The master branch is the development
|
||||
branch and contains features for the next release.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Go >= 1.7
|
||||
|
||||
- Go >= 1.7.1
|
||||
- [godep](https://github.com/tools/godep) installed
|
||||
- libtool development headers installed
|
||||
- Ubuntu: `apt-get install libltdl-dev`
|
||||
- CentOS/RedHat: `yum install libtool-ltdl-devel`
|
||||
- Mac OS ([Homebrew](http://brew.sh/)): `brew install libtool`
|
||||
|
||||
Run `make binaries`, which creates the Notary Client CLI binary at `bin/notary`.
|
||||
Note that `make binaries` assumes a standard Go directory structure, in which
|
||||
Run `make client`, which creates the Notary Client CLI binary at `bin/notary`.
|
||||
Note that `make client` assumes a standard Go directory structure, in which
|
||||
Notary is checked out to the `src` directory in your `GOPATH`. For example:
|
||||
```
|
||||
$GOPATH/
|
||||
@@ -98,3 +101,5 @@ $GOPATH/
|
||||
docker/
|
||||
notary/
|
||||
```
|
||||
|
||||
To build the server and signer, please run `docker-compose build`.
|
||||
+20
-22
@@ -8,10 +8,8 @@ import (
|
||||
// Unfortunately because of targets delegations, we can only
|
||||
// cover the base roles.
|
||||
const (
|
||||
ScopeRoot = "root"
|
||||
ScopeTargets = "targets"
|
||||
ScopeSnapshot = "snapshot"
|
||||
ScopeTimestamp = "timestamp"
|
||||
ScopeRoot = "root"
|
||||
ScopeTargets = "targets"
|
||||
)
|
||||
|
||||
// Types for TUFChanges are namespaced by the Role they
|
||||
@@ -20,7 +18,7 @@ const (
|
||||
// all changes in Snapshot and Timestamp are programmatically
|
||||
// generated base on Root and Targets changes.
|
||||
const (
|
||||
TypeRootRole = "role"
|
||||
TypeBaseRole = "role"
|
||||
TypeTargetsTarget = "target"
|
||||
TypeTargetsDelegation = "delegation"
|
||||
TypeWitness = "witness"
|
||||
@@ -29,22 +27,22 @@ const (
|
||||
// TUFChange represents a change to a TUF repo
|
||||
type TUFChange struct {
|
||||
// Abbreviated because Go doesn't permit a field and method of the same name
|
||||
Actn string `json:"action"`
|
||||
Role string `json:"role"`
|
||||
ChangeType string `json:"type"`
|
||||
ChangePath string `json:"path"`
|
||||
Data []byte `json:"data"`
|
||||
Actn string `json:"action"`
|
||||
Role data.RoleName `json:"role"`
|
||||
ChangeType string `json:"type"`
|
||||
ChangePath string `json:"path"`
|
||||
Data []byte `json:"data"`
|
||||
}
|
||||
|
||||
// TUFRootData represents a modification of the keys associated
|
||||
// with a role that appears in the root.json
|
||||
type TUFRootData struct {
|
||||
Keys data.KeyList `json:"keys"`
|
||||
RoleName string `json:"role"`
|
||||
Keys data.KeyList `json:"keys"`
|
||||
RoleName data.RoleName `json:"role"`
|
||||
}
|
||||
|
||||
// NewTUFChange initializes a TUFChange object
|
||||
func NewTUFChange(action string, role, changeType, changePath string, content []byte) *TUFChange {
|
||||
func NewTUFChange(action string, role data.RoleName, changeType, changePath string, content []byte) *TUFChange {
|
||||
return &TUFChange{
|
||||
Actn: action,
|
||||
Role: role,
|
||||
@@ -60,7 +58,7 @@ func (c TUFChange) Action() string {
|
||||
}
|
||||
|
||||
// Scope returns c.Role
|
||||
func (c TUFChange) Scope() string {
|
||||
func (c TUFChange) Scope() data.RoleName {
|
||||
return c.Role
|
||||
}
|
||||
|
||||
@@ -83,17 +81,17 @@ func (c TUFChange) Content() []byte {
|
||||
// this includes creating a delegations. This format is used to avoid
|
||||
// unexpected race conditions between humans modifying the same delegation
|
||||
type TUFDelegation struct {
|
||||
NewName string `json:"new_name,omitempty"`
|
||||
NewThreshold int `json:"threshold, omitempty"`
|
||||
AddKeys data.KeyList `json:"add_keys, omitempty"`
|
||||
RemoveKeys []string `json:"remove_keys,omitempty"`
|
||||
AddPaths []string `json:"add_paths,omitempty"`
|
||||
RemovePaths []string `json:"remove_paths,omitempty"`
|
||||
ClearAllPaths bool `json:"clear_paths,omitempty"`
|
||||
NewName data.RoleName `json:"new_name,omitempty"`
|
||||
NewThreshold int `json:"threshold, omitempty"`
|
||||
AddKeys data.KeyList `json:"add_keys, omitempty"`
|
||||
RemoveKeys []string `json:"remove_keys,omitempty"`
|
||||
AddPaths []string `json:"add_paths,omitempty"`
|
||||
RemovePaths []string `json:"remove_paths,omitempty"`
|
||||
ClearAllPaths bool `json:"clear_paths,omitempty"`
|
||||
}
|
||||
|
||||
// ToNewRole creates a fresh role object from the TUFDelegation data
|
||||
func (td TUFDelegation) ToNewRole(scope string) (*data.Role, error) {
|
||||
func (td TUFDelegation) ToNewRole(scope data.RoleName) (*data.Role, error) {
|
||||
name := scope
|
||||
if td.NewName != "" {
|
||||
name = td.NewName
|
||||
|
||||
+5
@@ -21,6 +21,11 @@ func (cl *memChangelist) Add(c Change) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Location returns the string "memory"
|
||||
func (cl memChangelist) Location() string {
|
||||
return "memory"
|
||||
}
|
||||
|
||||
// Remove deletes the changes found at the given indices
|
||||
func (cl *memChangelist) Remove(idxs []int) error {
|
||||
remove := make(map[int]struct{})
|
||||
|
||||
Generated
Vendored
+7
-2
@@ -5,12 +5,12 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/docker/distribution/uuid"
|
||||
"path/filepath"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// FileChangelist stores all the changes as files
|
||||
@@ -137,6 +137,11 @@ func (cl FileChangelist) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Location returns the file path to the changelist
|
||||
func (cl FileChangelist) Location() string {
|
||||
return cl.dir
|
||||
}
|
||||
|
||||
// NewIterator creates an iterator from FileChangelist
|
||||
func (cl FileChangelist) NewIterator() (ChangeIterator, error) {
|
||||
fileInfos, err := getFileNames(cl.dir)
|
||||
|
||||
+6
-1
@@ -1,5 +1,7 @@
|
||||
package changelist
|
||||
|
||||
import "github.com/docker/notary/tuf/data"
|
||||
|
||||
// Changelist is the interface for all TUF change lists
|
||||
type Changelist interface {
|
||||
// List returns the ordered list of changes
|
||||
@@ -25,6 +27,9 @@ type Changelist interface {
|
||||
// NewIterator returns an iterator for walking through the list
|
||||
// of changes currently stored
|
||||
NewIterator() (ChangeIterator, error)
|
||||
|
||||
// Location returns the place the changelist is stores
|
||||
Location() string
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -43,7 +48,7 @@ type Change interface {
|
||||
|
||||
// Where the change should be made.
|
||||
// For TUF this will be the role
|
||||
Scope() string
|
||||
Scope() data.RoleName
|
||||
|
||||
// The content type being affected.
|
||||
// For TUF this will be "target", or "delegation".
|
||||
|
||||
+621
-346
File diff suppressed because it is too large
Load Diff
+20
-57
@@ -3,19 +3,18 @@ package client
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/docker/notary"
|
||||
"github.com/docker/notary/client/changelist"
|
||||
store "github.com/docker/notary/storage"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/docker/notary/tuf/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// AddDelegation creates changelist entries to add provided delegation public keys and paths.
|
||||
// This method composes AddDelegationRoleAndKeys and AddDelegationPaths (each creates one changelist if called).
|
||||
func (r *NotaryRepository) AddDelegation(name string, delegationKeys []data.PublicKey, paths []string) error {
|
||||
func (r *repository) AddDelegation(name data.RoleName, delegationKeys []data.PublicKey, paths []string) error {
|
||||
if len(delegationKeys) > 0 {
|
||||
err := r.AddDelegationRoleAndKeys(name, delegationKeys)
|
||||
if err != nil {
|
||||
@@ -34,18 +33,12 @@ func (r *NotaryRepository) AddDelegation(name string, delegationKeys []data.Publ
|
||||
// AddDelegationRoleAndKeys creates a changelist entry to add provided delegation public keys.
|
||||
// This method is the simplest way to create a new delegation, because the delegation must have at least
|
||||
// one key upon creation to be valid since we will reject the changelist while validating the threshold.
|
||||
func (r *NotaryRepository) AddDelegationRoleAndKeys(name string, delegationKeys []data.PublicKey) error {
|
||||
func (r *repository) AddDelegationRoleAndKeys(name data.RoleName, delegationKeys []data.PublicKey) error {
|
||||
|
||||
if !data.IsDelegation(name) {
|
||||
return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"}
|
||||
}
|
||||
|
||||
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cl.Close()
|
||||
|
||||
logrus.Debugf(`Adding delegation "%s" with threshold %d, and %d keys\n`,
|
||||
name, notary.MinThreshold, len(delegationKeys))
|
||||
|
||||
@@ -59,23 +52,17 @@ func (r *NotaryRepository) AddDelegationRoleAndKeys(name string, delegationKeys
|
||||
}
|
||||
|
||||
template := newCreateDelegationChange(name, tdJSON)
|
||||
return addChange(cl, template, name)
|
||||
return addChange(r.changelist, template, name)
|
||||
}
|
||||
|
||||
// AddDelegationPaths creates a changelist entry to add provided paths to an existing delegation.
|
||||
// This method cannot create a new delegation itself because the role must meet the key threshold upon creation.
|
||||
func (r *NotaryRepository) AddDelegationPaths(name string, paths []string) error {
|
||||
func (r *repository) AddDelegationPaths(name data.RoleName, paths []string) error {
|
||||
|
||||
if !data.IsDelegation(name) {
|
||||
return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"}
|
||||
}
|
||||
|
||||
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cl.Close()
|
||||
|
||||
logrus.Debugf(`Adding %s paths to delegation %s\n`, paths, name)
|
||||
|
||||
tdJSON, err := json.Marshal(&changelist.TUFDelegation{
|
||||
@@ -86,12 +73,12 @@ func (r *NotaryRepository) AddDelegationPaths(name string, paths []string) error
|
||||
}
|
||||
|
||||
template := newCreateDelegationChange(name, tdJSON)
|
||||
return addChange(cl, template, name)
|
||||
return addChange(r.changelist, template, name)
|
||||
}
|
||||
|
||||
// RemoveDelegationKeysAndPaths creates changelist entries to remove provided delegation key IDs and paths.
|
||||
// This method composes RemoveDelegationPaths and RemoveDelegationKeys (each creates one changelist if called).
|
||||
func (r *NotaryRepository) RemoveDelegationKeysAndPaths(name string, keyIDs, paths []string) error {
|
||||
func (r *repository) RemoveDelegationKeysAndPaths(name data.RoleName, keyIDs, paths []string) error {
|
||||
if len(paths) > 0 {
|
||||
err := r.RemoveDelegationPaths(name, paths)
|
||||
if err != nil {
|
||||
@@ -108,37 +95,25 @@ func (r *NotaryRepository) RemoveDelegationKeysAndPaths(name string, keyIDs, pat
|
||||
}
|
||||
|
||||
// RemoveDelegationRole creates a changelist to remove all paths and keys from a role, and delete the role in its entirety.
|
||||
func (r *NotaryRepository) RemoveDelegationRole(name string) error {
|
||||
func (r *repository) RemoveDelegationRole(name data.RoleName) error {
|
||||
|
||||
if !data.IsDelegation(name) {
|
||||
return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"}
|
||||
}
|
||||
|
||||
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cl.Close()
|
||||
|
||||
logrus.Debugf(`Removing delegation "%s"\n`, name)
|
||||
|
||||
template := newDeleteDelegationChange(name, nil)
|
||||
return addChange(cl, template, name)
|
||||
return addChange(r.changelist, template, name)
|
||||
}
|
||||
|
||||
// RemoveDelegationPaths creates a changelist entry to remove provided paths from an existing delegation.
|
||||
func (r *NotaryRepository) RemoveDelegationPaths(name string, paths []string) error {
|
||||
func (r *repository) RemoveDelegationPaths(name data.RoleName, paths []string) error {
|
||||
|
||||
if !data.IsDelegation(name) {
|
||||
return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"}
|
||||
}
|
||||
|
||||
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cl.Close()
|
||||
|
||||
logrus.Debugf(`Removing %s paths from delegation "%s"\n`, paths, name)
|
||||
|
||||
tdJSON, err := json.Marshal(&changelist.TUFDelegation{
|
||||
@@ -149,7 +124,7 @@ func (r *NotaryRepository) RemoveDelegationPaths(name string, paths []string) er
|
||||
}
|
||||
|
||||
template := newUpdateDelegationChange(name, tdJSON)
|
||||
return addChange(cl, template, name)
|
||||
return addChange(r.changelist, template, name)
|
||||
}
|
||||
|
||||
// RemoveDelegationKeys creates a changelist entry to remove provided keys from an existing delegation.
|
||||
@@ -157,18 +132,12 @@ func (r *NotaryRepository) RemoveDelegationPaths(name string, paths []string) er
|
||||
// the role itself will be deleted in its entirety.
|
||||
// It can also delete a key from all delegations under a parent using a name
|
||||
// with a wildcard at the end.
|
||||
func (r *NotaryRepository) RemoveDelegationKeys(name string, keyIDs []string) error {
|
||||
func (r *repository) RemoveDelegationKeys(name data.RoleName, keyIDs []string) error {
|
||||
|
||||
if !data.IsDelegation(name) && !data.IsWildDelegation(name) {
|
||||
return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"}
|
||||
}
|
||||
|
||||
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cl.Close()
|
||||
|
||||
logrus.Debugf(`Removing %s keys from delegation "%s"\n`, keyIDs, name)
|
||||
|
||||
tdJSON, err := json.Marshal(&changelist.TUFDelegation{
|
||||
@@ -179,22 +148,16 @@ func (r *NotaryRepository) RemoveDelegationKeys(name string, keyIDs []string) er
|
||||
}
|
||||
|
||||
template := newUpdateDelegationChange(name, tdJSON)
|
||||
return addChange(cl, template, name)
|
||||
return addChange(r.changelist, template, name)
|
||||
}
|
||||
|
||||
// ClearDelegationPaths creates a changelist entry to remove all paths from an existing delegation.
|
||||
func (r *NotaryRepository) ClearDelegationPaths(name string) error {
|
||||
func (r *repository) ClearDelegationPaths(name data.RoleName) error {
|
||||
|
||||
if !data.IsDelegation(name) {
|
||||
return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"}
|
||||
}
|
||||
|
||||
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cl.Close()
|
||||
|
||||
logrus.Debugf(`Removing all paths from delegation "%s"\n`, name)
|
||||
|
||||
tdJSON, err := json.Marshal(&changelist.TUFDelegation{
|
||||
@@ -205,10 +168,10 @@ func (r *NotaryRepository) ClearDelegationPaths(name string) error {
|
||||
}
|
||||
|
||||
template := newUpdateDelegationChange(name, tdJSON)
|
||||
return addChange(cl, template, name)
|
||||
return addChange(r.changelist, template, name)
|
||||
}
|
||||
|
||||
func newUpdateDelegationChange(name string, content []byte) *changelist.TUFChange {
|
||||
func newUpdateDelegationChange(name data.RoleName, content []byte) *changelist.TUFChange {
|
||||
return changelist.NewTUFChange(
|
||||
changelist.ActionUpdate,
|
||||
name,
|
||||
@@ -218,7 +181,7 @@ func newUpdateDelegationChange(name string, content []byte) *changelist.TUFChang
|
||||
)
|
||||
}
|
||||
|
||||
func newCreateDelegationChange(name string, content []byte) *changelist.TUFChange {
|
||||
func newCreateDelegationChange(name data.RoleName, content []byte) *changelist.TUFChange {
|
||||
return changelist.NewTUFChange(
|
||||
changelist.ActionCreate,
|
||||
name,
|
||||
@@ -228,7 +191,7 @@ func newCreateDelegationChange(name string, content []byte) *changelist.TUFChang
|
||||
)
|
||||
}
|
||||
|
||||
func newDeleteDelegationChange(name string, content []byte) *changelist.TUFChange {
|
||||
func newDeleteDelegationChange(name data.RoleName, content []byte) *changelist.TUFChange {
|
||||
return changelist.NewTUFChange(
|
||||
changelist.ActionDelete,
|
||||
name,
|
||||
@@ -240,7 +203,7 @@ func newDeleteDelegationChange(name string, content []byte) *changelist.TUFChang
|
||||
|
||||
// GetDelegationRoles returns the keys and roles of the repository's delegations
|
||||
// Also converts key IDs to canonical key IDs to keep consistent with signing prompts
|
||||
func (r *NotaryRepository) GetDelegationRoles() ([]data.Role, error) {
|
||||
func (r *repository) GetDelegationRoles() ([]data.Role, error) {
|
||||
// Update state of the repo to latest
|
||||
if err := r.Update(false); err != nil {
|
||||
return nil, err
|
||||
@@ -249,7 +212,7 @@ func (r *NotaryRepository) GetDelegationRoles() ([]data.Role, error) {
|
||||
// All top level delegations (ex: targets/level1) are stored exclusively in targets.json
|
||||
_, ok := r.tufRepo.Targets[data.CanonicalTargetsRole]
|
||||
if !ok {
|
||||
return nil, store.ErrMetaNotFound{Resource: data.CanonicalTargetsRole}
|
||||
return nil, store.ErrMetaNotFound{Resource: data.CanonicalTargetsRole.String()}
|
||||
}
|
||||
|
||||
// make a copy for traversing nested delegations
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/notary/tuf/data"
|
||||
)
|
||||
|
||||
// ErrRepoNotInitialized is returned when trying to publish an uninitialized
|
||||
// notary repository
|
||||
type ErrRepoNotInitialized struct{}
|
||||
|
||||
func (err ErrRepoNotInitialized) Error() string {
|
||||
return "repository has not been initialized"
|
||||
}
|
||||
|
||||
// ErrInvalidRemoteRole is returned when the server is requested to manage
|
||||
// a key type that is not permitted
|
||||
type ErrInvalidRemoteRole struct {
|
||||
Role data.RoleName
|
||||
}
|
||||
|
||||
func (err ErrInvalidRemoteRole) Error() string {
|
||||
return fmt.Sprintf(
|
||||
"notary does not permit the server managing the %s key", err.Role.String())
|
||||
}
|
||||
|
||||
// ErrInvalidLocalRole is returned when the client wants to manage
|
||||
// a key type that is not permitted
|
||||
type ErrInvalidLocalRole struct {
|
||||
Role data.RoleName
|
||||
}
|
||||
|
||||
func (err ErrInvalidLocalRole) Error() string {
|
||||
return fmt.Sprintf(
|
||||
"notary does not permit the client managing the %s key", err.Role)
|
||||
}
|
||||
|
||||
// ErrRepositoryNotExist is returned when an action is taken on a remote
|
||||
// repository that doesn't exist
|
||||
type ErrRepositoryNotExist struct {
|
||||
remote string
|
||||
gun data.GUN
|
||||
}
|
||||
|
||||
func (err ErrRepositoryNotExist) Error() string {
|
||||
return fmt.Sprintf("%s does not have trust data for %s", err.remote, err.gun.String())
|
||||
}
|
||||
+45
-17
@@ -10,14 +10,15 @@ import (
|
||||
store "github.com/docker/notary/storage"
|
||||
"github.com/docker/notary/tuf"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/docker/notary/tuf/signed"
|
||||
"github.com/docker/notary/tuf/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Use this to initialize remote HTTPStores from the config settings
|
||||
func getRemoteStore(baseURL, gun string, rt http.RoundTripper) (store.RemoteStore, error) {
|
||||
func getRemoteStore(baseURL string, gun data.GUN, rt http.RoundTripper) (store.RemoteStore, error) {
|
||||
s, err := store.NewHTTPStore(
|
||||
baseURL+"/v2/"+gun+"/_trust/tuf/",
|
||||
baseURL+"/v2/"+gun.String()+"/_trust/tuf/",
|
||||
"",
|
||||
"json",
|
||||
"key",
|
||||
@@ -26,7 +27,7 @@ func getRemoteStore(baseURL, gun string, rt http.RoundTripper) (store.RemoteStor
|
||||
if err != nil {
|
||||
return store.OfflineStore{}, err
|
||||
}
|
||||
return s, err
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func applyChangelist(repo *tuf.Repo, invalid *tuf.Repo, cl changelist.Changelist) error {
|
||||
@@ -47,7 +48,7 @@ func applyChangelist(repo *tuf.Repo, invalid *tuf.Repo, cl changelist.Changelist
|
||||
case c.Scope() == changelist.ScopeRoot:
|
||||
err = applyRootChange(repo, c)
|
||||
default:
|
||||
return fmt.Errorf("scope not supported: %s", c.Scope())
|
||||
return fmt.Errorf("scope not supported: %s", c.Scope().String())
|
||||
}
|
||||
if err != nil {
|
||||
logrus.Debugf("error attempting to apply change #%d: %s, on scope: %s path: %s type: %s", index, c.Action(), c.Scope(), c.Path(), c.Type())
|
||||
@@ -165,7 +166,7 @@ func changeTargetMeta(repo *tuf.Repo, c changelist.Change) error {
|
||||
func applyRootChange(repo *tuf.Repo, c changelist.Change) error {
|
||||
var err error
|
||||
switch c.Type() {
|
||||
case changelist.TypeRootRole:
|
||||
case changelist.TypeBaseRole:
|
||||
err = applyRootRoleChange(repo, c)
|
||||
default:
|
||||
err = fmt.Errorf("type of root change not yet supported: %s", c.Type())
|
||||
@@ -218,11 +219,7 @@ func warnRolesNearExpiry(r *tuf.Repo) {
|
||||
}
|
||||
|
||||
// Fetches a public key from a remote store, given a gun and role
|
||||
func getRemoteKey(url, gun, role string, rt http.RoundTripper) (data.PublicKey, error) {
|
||||
remote, err := getRemoteStore(url, gun, rt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func getRemoteKey(role data.RoleName, remote store.RemoteStore) (data.PublicKey, error) {
|
||||
rawPubKey, err := remote.GetKey(role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -237,11 +234,7 @@ func getRemoteKey(url, gun, role string, rt http.RoundTripper) (data.PublicKey,
|
||||
}
|
||||
|
||||
// Rotates a private key in a remote store and returns the public key component
|
||||
func rotateRemoteKey(url, gun, role string, rt http.RoundTripper) (data.PublicKey, error) {
|
||||
remote, err := getRemoteStore(url, gun, rt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func rotateRemoteKey(role data.RoleName, remote store.RemoteStore) (data.PublicKey, error) {
|
||||
rawPubKey, err := remote.RotateKey(role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -256,11 +249,11 @@ func rotateRemoteKey(url, gun, role string, rt http.RoundTripper) (data.PublicKe
|
||||
}
|
||||
|
||||
// signs and serializes the metadata for a canonical role in a TUF repo to JSON
|
||||
func serializeCanonicalRole(tufRepo *tuf.Repo, role string) (out []byte, err error) {
|
||||
func serializeCanonicalRole(tufRepo *tuf.Repo, role data.RoleName, extraSigningKeys data.KeyList) (out []byte, err error) {
|
||||
var s *data.Signed
|
||||
switch {
|
||||
case role == data.CanonicalRootRole:
|
||||
s, err = tufRepo.SignRoot(data.DefaultExpires(role))
|
||||
s, err = tufRepo.SignRoot(data.DefaultExpires(role), extraSigningKeys)
|
||||
case role == data.CanonicalSnapshotRole:
|
||||
s, err = tufRepo.SignSnapshot(data.DefaultExpires(role))
|
||||
case tufRepo.Targets[role] != nil:
|
||||
@@ -276,3 +269,38 @@ func serializeCanonicalRole(tufRepo *tuf.Repo, role string) (out []byte, err err
|
||||
|
||||
return json.Marshal(s)
|
||||
}
|
||||
|
||||
func getAllPrivKeys(rootKeyIDs []string, cryptoService signed.CryptoService) ([]data.PrivateKey, error) {
|
||||
if cryptoService == nil {
|
||||
return nil, fmt.Errorf("no crypto service available to get private keys from")
|
||||
}
|
||||
|
||||
privKeys := make([]data.PrivateKey, 0, len(rootKeyIDs))
|
||||
for _, keyID := range rootKeyIDs {
|
||||
privKey, _, err := cryptoService.GetPrivateKey(keyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
privKeys = append(privKeys, privKey)
|
||||
}
|
||||
if len(privKeys) == 0 {
|
||||
var rootKeyID string
|
||||
rootKeyList := cryptoService.ListKeys(data.CanonicalRootRole)
|
||||
if len(rootKeyList) == 0 {
|
||||
rootPublicKey, err := cryptoService.Create(data.CanonicalRootRole, "", data.ECDSAKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rootKeyID = rootPublicKey.ID()
|
||||
} else {
|
||||
rootKeyID = rootKeyList[0]
|
||||
}
|
||||
privKey, _, err := cryptoService.GetPrivateKey(rootKeyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
privKeys = append(privKeys, privKey)
|
||||
}
|
||||
|
||||
return privKeys, nil
|
||||
}
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"github.com/docker/notary/client/changelist"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/docker/notary/tuf/signed"
|
||||
)
|
||||
|
||||
// Repository represents the set of options that must be supported over a TUF repo.
|
||||
type Repository interface {
|
||||
// General management operations
|
||||
Initialize(rootKeyIDs []string, serverManagedRoles ...data.RoleName) error
|
||||
InitializeWithCertificate(rootKeyIDs []string, rootCerts []data.PublicKey, serverManagedRoles ...data.RoleName) error
|
||||
Publish() error
|
||||
|
||||
// Target Operations
|
||||
AddTarget(target *Target, roles ...data.RoleName) error
|
||||
RemoveTarget(targetName string, roles ...data.RoleName) error
|
||||
ListTargets(roles ...data.RoleName) ([]*TargetWithRole, error)
|
||||
GetTargetByName(name string, roles ...data.RoleName) (*TargetWithRole, error)
|
||||
GetAllTargetMetadataByName(name string) ([]TargetSignedStruct, error)
|
||||
|
||||
// Changelist operations
|
||||
GetChangelist() (changelist.Changelist, error)
|
||||
|
||||
// Role operations
|
||||
ListRoles() ([]RoleWithSignatures, error)
|
||||
GetDelegationRoles() ([]data.Role, error)
|
||||
AddDelegation(name data.RoleName, delegationKeys []data.PublicKey, paths []string) error
|
||||
AddDelegationRoleAndKeys(name data.RoleName, delegationKeys []data.PublicKey) error
|
||||
AddDelegationPaths(name data.RoleName, paths []string) error
|
||||
RemoveDelegationKeysAndPaths(name data.RoleName, keyIDs, paths []string) error
|
||||
RemoveDelegationRole(name data.RoleName) error
|
||||
RemoveDelegationPaths(name data.RoleName, paths []string) error
|
||||
RemoveDelegationKeys(name data.RoleName, keyIDs []string) error
|
||||
ClearDelegationPaths(name data.RoleName) error
|
||||
|
||||
// Witness and other re-signing operations
|
||||
Witness(roles ...data.RoleName) ([]data.RoleName, error)
|
||||
|
||||
// Key Operations
|
||||
RotateKey(role data.RoleName, serverManagesKey bool, keyList []string) error
|
||||
|
||||
GetCryptoService() signed.CryptoService
|
||||
SetLegacyVersions(int)
|
||||
GetGUN() data.GUN
|
||||
}
|
||||
+2
-13
@@ -4,26 +4,15 @@ package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/notary"
|
||||
"github.com/docker/notary/trustmanager"
|
||||
"github.com/docker/notary/trustpinning"
|
||||
)
|
||||
|
||||
// NewNotaryRepository is a helper method that returns a new notary repository.
|
||||
// It takes the base directory under where all the trust files will be stored
|
||||
// (This is normally defaults to "~/.notary" or "~/.docker/trust" when enabling
|
||||
// docker content trust).
|
||||
func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper,
|
||||
retriever notary.PassRetriever, trustPinning trustpinning.TrustPinConfig) (
|
||||
*NotaryRepository, error) {
|
||||
|
||||
func getKeyStores(baseDir string, retriever notary.PassRetriever) ([]trustmanager.KeyStore, error) {
|
||||
fileKeyStore, err := trustmanager.NewKeyFileStore(baseDir, retriever)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create private key store in directory: %s", baseDir)
|
||||
}
|
||||
|
||||
return repositoryFromKeystores(baseDir, gun, baseURL, rt,
|
||||
[]trustmanager.KeyStore{fileKeyStore}, trustPinning)
|
||||
return []trustmanager.KeyStore{fileKeyStore}, nil
|
||||
}
|
||||
|
||||
+2
-11
@@ -4,21 +4,13 @@ package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/notary"
|
||||
"github.com/docker/notary/trustmanager"
|
||||
"github.com/docker/notary/trustmanager/yubikey"
|
||||
"github.com/docker/notary/trustpinning"
|
||||
)
|
||||
|
||||
// NewNotaryRepository is a helper method that returns a new notary repository.
|
||||
// It takes the base directory under where all the trust files will be stored
|
||||
// (usually ~/.docker/trust/).
|
||||
func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper,
|
||||
retriever notary.PassRetriever, trustPinning trustpinning.TrustPinConfig) (
|
||||
*NotaryRepository, error) {
|
||||
|
||||
func getKeyStores(baseDir string, retriever notary.PassRetriever) ([]trustmanager.KeyStore, error) {
|
||||
fileKeyStore, err := trustmanager.NewKeyFileStore(baseDir, retriever)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create private key store in directory: %s", baseDir)
|
||||
@@ -29,6 +21,5 @@ func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper,
|
||||
if yubiKeyStore != nil {
|
||||
keyStores = []trustmanager.KeyStore{yubiKeyStore, fileKeyStore}
|
||||
}
|
||||
|
||||
return repositoryFromKeystores(baseDir, gun, baseURL, rt, keyStores, trustPinning)
|
||||
return keyStores, nil
|
||||
}
|
||||
|
||||
+120
-34
@@ -2,26 +2,28 @@ package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/docker/notary"
|
||||
store "github.com/docker/notary/storage"
|
||||
"github.com/docker/notary/trustpinning"
|
||||
"github.com/docker/notary/tuf"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/docker/notary/tuf/signed"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// TUFClient is a usability wrapper around a raw TUF repo
|
||||
type TUFClient struct {
|
||||
// tufClient is a usability wrapper around a raw TUF repo
|
||||
type tufClient struct {
|
||||
remote store.RemoteStore
|
||||
cache store.MetadataStore
|
||||
oldBuilder tuf.RepoBuilder
|
||||
newBuilder tuf.RepoBuilder
|
||||
}
|
||||
|
||||
// NewTUFClient initialized a TUFClient with the given repo, remote source of content, and cache
|
||||
func NewTUFClient(oldBuilder, newBuilder tuf.RepoBuilder, remote store.RemoteStore, cache store.MetadataStore) *TUFClient {
|
||||
return &TUFClient{
|
||||
// newTufClient initialized a tufClient with the given repo, remote source of content, and cache
|
||||
func newTufClient(oldBuilder, newBuilder tuf.RepoBuilder, remote store.RemoteStore, cache store.MetadataStore) *tufClient {
|
||||
return &tufClient{
|
||||
oldBuilder: oldBuilder,
|
||||
newBuilder: newBuilder,
|
||||
remote: remote,
|
||||
@@ -30,7 +32,7 @@ func NewTUFClient(oldBuilder, newBuilder tuf.RepoBuilder, remote store.RemoteSto
|
||||
}
|
||||
|
||||
// Update performs an update to the TUF repo as defined by the TUF spec
|
||||
func (c *TUFClient) Update() (*tuf.Repo, *tuf.Repo, error) {
|
||||
func (c *tufClient) Update() (*tuf.Repo, *tuf.Repo, error) {
|
||||
// 1. Get timestamp
|
||||
// a. If timestamp error (verification, expired, etc...) download new root and return to 1.
|
||||
// 2. Check if local snapshot is up to date
|
||||
@@ -47,8 +49,8 @@ func (c *TUFClient) Update() (*tuf.Repo, *tuf.Repo, error) {
|
||||
|
||||
c.newBuilder = c.newBuilder.BootstrapNewBuilder()
|
||||
|
||||
if err := c.downloadRoot(); err != nil {
|
||||
logrus.Debug("Client Update (Root):", err)
|
||||
if err := c.updateRoot(); err != nil {
|
||||
logrus.Debug("Client Update (Root): ", err)
|
||||
return nil, nil, err
|
||||
}
|
||||
// If we error again, we now have the latest root and just want to fail
|
||||
@@ -61,7 +63,7 @@ func (c *TUFClient) Update() (*tuf.Repo, *tuf.Repo, error) {
|
||||
return c.newBuilder.Finish()
|
||||
}
|
||||
|
||||
func (c *TUFClient) update() error {
|
||||
func (c *tufClient) update() error {
|
||||
if err := c.downloadTimestamp(); err != nil {
|
||||
logrus.Debugf("Client Update (Timestamp): %s", err.Error())
|
||||
return err
|
||||
@@ -78,37 +80,103 @@ func (c *TUFClient) update() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// downloadRoot is responsible for downloading the root.json
|
||||
func (c *TUFClient) downloadRoot() error {
|
||||
role := data.CanonicalRootRole
|
||||
consistentInfo := c.newBuilder.GetConsistentInfo(role)
|
||||
// updateRoot checks if there is a newer version of the root available, and if so
|
||||
// downloads all intermediate root files to allow proper key rotation.
|
||||
func (c *tufClient) updateRoot() error {
|
||||
// Get current root version
|
||||
currentRootConsistentInfo := c.oldBuilder.GetConsistentInfo(data.CanonicalRootRole)
|
||||
currentVersion := c.oldBuilder.GetLoadedVersion(currentRootConsistentInfo.RoleName)
|
||||
|
||||
// We can't read an exact size for the root metadata without risking getting stuck in the TUF update cycle
|
||||
// since it's possible that downloading timestamp/snapshot metadata may fail due to a signature mismatch
|
||||
if !consistentInfo.ChecksumKnown() {
|
||||
logrus.Debugf("Loading root with no expected checksum")
|
||||
// Get new root version
|
||||
raw, err := c.downloadRoot()
|
||||
|
||||
// get the cached root, if it exists, just for version checking
|
||||
cachedRoot, _ := c.cache.GetSized(role, -1)
|
||||
// prefer to download a new root
|
||||
_, remoteErr := c.tryLoadRemote(consistentInfo, cachedRoot)
|
||||
return remoteErr
|
||||
switch err.(type) {
|
||||
case *trustpinning.ErrRootRotationFail:
|
||||
// Rotation errors are okay since we haven't yet downloaded
|
||||
// all intermediate root files
|
||||
break
|
||||
case nil:
|
||||
// No error updating root - we were at most 1 version behind
|
||||
return nil
|
||||
default:
|
||||
// Return any non-rotation error.
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := c.tryLoadCacheThenRemote(consistentInfo)
|
||||
return err
|
||||
// Load current version into newBuilder
|
||||
currentRaw, err := c.cache.GetSized(data.CanonicalRootRole.String(), -1)
|
||||
if err != nil {
|
||||
logrus.Debugf("error loading %d.%s: %s", currentVersion, data.CanonicalRootRole, err)
|
||||
return err
|
||||
}
|
||||
if err := c.newBuilder.LoadRootForUpdate(currentRaw, currentVersion, false); err != nil {
|
||||
logrus.Debugf("%d.%s is invalid: %s", currentVersion, data.CanonicalRootRole, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Extract newest version number
|
||||
signedRoot := &data.Signed{}
|
||||
if err := json.Unmarshal(raw, signedRoot); err != nil {
|
||||
return err
|
||||
}
|
||||
newestRoot, err := data.RootFromSigned(signedRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newestVersion := newestRoot.Signed.SignedCommon.Version
|
||||
|
||||
// Update from current + 1 (current already loaded) to newest - 1 (newest loaded below)
|
||||
if err := c.updateRootVersions(currentVersion+1, newestVersion-1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Already downloaded newest, verify it against newest - 1
|
||||
if err := c.newBuilder.LoadRootForUpdate(raw, newestVersion, true); err != nil {
|
||||
logrus.Debugf("downloaded %d.%s is invalid: %s", newestVersion, data.CanonicalRootRole, err)
|
||||
return err
|
||||
}
|
||||
logrus.Debugf("successfully verified downloaded %d.%s", newestVersion, data.CanonicalRootRole)
|
||||
|
||||
// Write newest to cache
|
||||
if err := c.cache.Set(data.CanonicalRootRole.String(), raw); err != nil {
|
||||
logrus.Debugf("unable to write %s to cache: %d.%s", newestVersion, data.CanonicalRootRole, err)
|
||||
}
|
||||
logrus.Debugf("finished updating root files")
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateRootVersions updates the root from it's current version to a target, rotating keys
|
||||
// as they are found
|
||||
func (c *tufClient) updateRootVersions(fromVersion, toVersion int) error {
|
||||
for v := fromVersion; v <= toVersion; v++ {
|
||||
logrus.Debugf("updating root from version %d to version %d, currently fetching %d", fromVersion, toVersion, v)
|
||||
|
||||
versionedRole := fmt.Sprintf("%d.%s", v, data.CanonicalRootRole)
|
||||
|
||||
raw, err := c.remote.GetSized(versionedRole, -1)
|
||||
if err != nil {
|
||||
logrus.Debugf("error downloading %s: %s", versionedRole, err)
|
||||
return err
|
||||
}
|
||||
if err := c.newBuilder.LoadRootForUpdate(raw, v, false); err != nil {
|
||||
logrus.Debugf("downloaded %s is invalid: %s", versionedRole, err)
|
||||
return err
|
||||
}
|
||||
logrus.Debugf("successfully verified downloaded %s", versionedRole)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// downloadTimestamp is responsible for downloading the timestamp.json
|
||||
// Timestamps are special in that we ALWAYS attempt to download and only
|
||||
// use cache if the download fails (and the cache is still valid).
|
||||
func (c *TUFClient) downloadTimestamp() error {
|
||||
func (c *tufClient) downloadTimestamp() error {
|
||||
logrus.Debug("Loading timestamp...")
|
||||
role := data.CanonicalTimestampRole
|
||||
consistentInfo := c.newBuilder.GetConsistentInfo(role)
|
||||
|
||||
// always get the remote timestamp, since it supersedes the local one
|
||||
cachedTS, cachedErr := c.cache.GetSized(role, notary.MaxTimestampSize)
|
||||
cachedTS, cachedErr := c.cache.GetSized(role.String(), notary.MaxTimestampSize)
|
||||
_, remoteErr := c.tryLoadRemote(consistentInfo, cachedTS)
|
||||
|
||||
// check that there was no remote error, or if there was a network problem
|
||||
@@ -138,7 +206,7 @@ func (c *TUFClient) downloadTimestamp() error {
|
||||
}
|
||||
|
||||
// downloadSnapshot is responsible for downloading the snapshot.json
|
||||
func (c *TUFClient) downloadSnapshot() error {
|
||||
func (c *tufClient) downloadSnapshot() error {
|
||||
logrus.Debug("Loading snapshot...")
|
||||
role := data.CanonicalSnapshotRole
|
||||
consistentInfo := c.newBuilder.GetConsistentInfo(role)
|
||||
@@ -150,7 +218,7 @@ func (c *TUFClient) downloadSnapshot() error {
|
||||
// downloadTargets downloads all targets and delegated targets for the repository.
|
||||
// It uses a pre-order tree traversal as it's necessary to download parents first
|
||||
// to obtain the keys to validate children.
|
||||
func (c *TUFClient) downloadTargets() error {
|
||||
func (c *tufClient) downloadTargets() error {
|
||||
toDownload := []data.DelegationRole{{
|
||||
BaseRole: data.BaseRole{Name: data.CanonicalTargetsRole},
|
||||
Paths: []string{""},
|
||||
@@ -183,7 +251,7 @@ func (c *TUFClient) downloadTargets() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c TUFClient) getTargetsFile(role data.DelegationRole, ci tuf.ConsistentInfo) ([]data.DelegationRole, error) {
|
||||
func (c tufClient) getTargetsFile(role data.DelegationRole, ci tuf.ConsistentInfo) ([]data.DelegationRole, error) {
|
||||
logrus.Debugf("Loading %s...", role.Name)
|
||||
tgs := &data.SignedTargets{}
|
||||
|
||||
@@ -198,8 +266,26 @@ func (c TUFClient) getTargetsFile(role data.DelegationRole, ci tuf.ConsistentInf
|
||||
return tgs.GetValidDelegations(role), nil
|
||||
}
|
||||
|
||||
func (c *TUFClient) tryLoadCacheThenRemote(consistentInfo tuf.ConsistentInfo) ([]byte, error) {
|
||||
cachedTS, err := c.cache.GetSized(consistentInfo.RoleName, consistentInfo.Length())
|
||||
// downloadRoot is responsible for downloading the root.json
|
||||
func (c *tufClient) downloadRoot() ([]byte, error) {
|
||||
role := data.CanonicalRootRole
|
||||
consistentInfo := c.newBuilder.GetConsistentInfo(role)
|
||||
|
||||
// We can't read an exact size for the root metadata without risking getting stuck in the TUF update cycle
|
||||
// since it's possible that downloading timestamp/snapshot metadata may fail due to a signature mismatch
|
||||
if !consistentInfo.ChecksumKnown() {
|
||||
logrus.Debugf("Loading root with no expected checksum")
|
||||
|
||||
// get the cached root, if it exists, just for version checking
|
||||
cachedRoot, _ := c.cache.GetSized(role.String(), -1)
|
||||
// prefer to download a new root
|
||||
return c.tryLoadRemote(consistentInfo, cachedRoot)
|
||||
}
|
||||
return c.tryLoadCacheThenRemote(consistentInfo)
|
||||
}
|
||||
|
||||
func (c *tufClient) tryLoadCacheThenRemote(consistentInfo tuf.ConsistentInfo) ([]byte, error) {
|
||||
cachedTS, err := c.cache.GetSized(consistentInfo.RoleName.String(), consistentInfo.Length())
|
||||
if err != nil {
|
||||
logrus.Debugf("no %s in cache, must download", consistentInfo.RoleName)
|
||||
return c.tryLoadRemote(consistentInfo, nil)
|
||||
@@ -214,7 +300,7 @@ func (c *TUFClient) tryLoadCacheThenRemote(consistentInfo tuf.ConsistentInfo) ([
|
||||
return c.tryLoadRemote(consistentInfo, cachedTS)
|
||||
}
|
||||
|
||||
func (c *TUFClient) tryLoadRemote(consistentInfo tuf.ConsistentInfo, old []byte) ([]byte, error) {
|
||||
func (c *tufClient) tryLoadRemote(consistentInfo tuf.ConsistentInfo, old []byte) ([]byte, error) {
|
||||
consistentName := consistentInfo.ConsistentName()
|
||||
raw, err := c.remote.GetSized(consistentName, consistentInfo.Length())
|
||||
if err != nil {
|
||||
@@ -232,7 +318,7 @@ func (c *TUFClient) tryLoadRemote(consistentInfo tuf.ConsistentInfo, old []byte)
|
||||
return raw, err
|
||||
}
|
||||
logrus.Debugf("successfully verified downloaded %s", consistentName)
|
||||
if err := c.cache.Set(consistentInfo.RoleName, raw); err != nil {
|
||||
if err := c.cache.Set(consistentInfo.RoleName.String(), raw); err != nil {
|
||||
logrus.Debugf("Unable to write %s to cache: %s", consistentInfo.RoleName, err)
|
||||
}
|
||||
return raw, nil
|
||||
|
||||
+5
-12
@@ -1,8 +1,6 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/notary/client/changelist"
|
||||
"github.com/docker/notary/tuf"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
@@ -10,14 +8,9 @@ import (
|
||||
|
||||
// Witness creates change objects to witness (i.e. re-sign) the given
|
||||
// roles on the next publish. One change is created per role
|
||||
func (r *NotaryRepository) Witness(roles ...string) ([]string, error) {
|
||||
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cl.Close()
|
||||
|
||||
successful := make([]string, 0, len(roles))
|
||||
func (r *repository) Witness(roles ...data.RoleName) ([]data.RoleName, error) {
|
||||
var err error
|
||||
successful := make([]data.RoleName, 0, len(roles))
|
||||
for _, role := range roles {
|
||||
// scope is role
|
||||
c := changelist.NewTUFChange(
|
||||
@@ -27,7 +20,7 @@ func (r *NotaryRepository) Witness(roles ...string) ([]string, error) {
|
||||
"",
|
||||
nil,
|
||||
)
|
||||
err = cl.Add(c)
|
||||
err = r.changelist.Add(c)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
@@ -36,7 +29,7 @@ func (r *NotaryRepository) Witness(roles ...string) ([]string, error) {
|
||||
return successful, err
|
||||
}
|
||||
|
||||
func witnessTargets(repo *tuf.Repo, invalid *tuf.Repo, role string) error {
|
||||
func witnessTargets(repo *tuf.Repo, invalid *tuf.Repo, role data.RoleName) error {
|
||||
if r, ok := repo.Targets[role]; ok {
|
||||
// role is already valid, mark for re-signing/updating
|
||||
r.Dirty = true
|
||||
|
||||
+41
-16
@@ -1,6 +1,8 @@
|
||||
package notary
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// application wide constants
|
||||
const (
|
||||
@@ -12,14 +14,10 @@ const (
|
||||
MinRSABitSize = 2048
|
||||
// MinThreshold requires a minimum of one threshold for roles; currently we do not support a higher threshold
|
||||
MinThreshold = 1
|
||||
// PrivKeyPerms are the file permissions to use when writing private keys to disk
|
||||
PrivKeyPerms = 0700
|
||||
// PubCertPerms are the file permissions to use when writing public certificates to disk
|
||||
PubCertPerms = 0755
|
||||
// Sha256HexSize is how big a Sha256 hex is in number of characters
|
||||
Sha256HexSize = 64
|
||||
// Sha512HexSize is how big a Sha512 hex is in number of characters
|
||||
Sha512HexSize = 128
|
||||
// SHA256HexSize is how big a SHA256 hex is in number of characters
|
||||
SHA256HexSize = 64
|
||||
// SHA512HexSize is how big a SHA512 hex is in number of characters
|
||||
SHA512HexSize = 128
|
||||
// SHA256 is the name of SHA256 hash algorithm
|
||||
SHA256 = "sha256"
|
||||
// SHA512 is the name of SHA512 hash algorithm
|
||||
@@ -29,8 +27,10 @@ const (
|
||||
// PrivDir is the directory, under the notary repo base directory, where private keys are stored
|
||||
PrivDir = "private"
|
||||
// RootKeysSubdir is the subdirectory under PrivDir where root private keys are stored
|
||||
// DEPRECATED: The only reason we need this constant is compatibility with older versions
|
||||
RootKeysSubdir = "root_keys"
|
||||
// NonRootKeysSubdir is the subdirectory under PrivDir where non-root private keys are stored
|
||||
// DEPRECATED: The only reason we need this constant is compatibility with older versions
|
||||
NonRootKeysSubdir = "tuf_keys"
|
||||
// KeyExtension is the file extension to use for private key files
|
||||
KeyExtension = "key"
|
||||
@@ -54,17 +54,42 @@ const (
|
||||
|
||||
MySQLBackend = "mysql"
|
||||
MemoryBackend = "memory"
|
||||
PostgresBackend = "postgres"
|
||||
SQLiteBackend = "sqlite3"
|
||||
RethinkDBBackend = "rethinkdb"
|
||||
FileBackend = "file"
|
||||
|
||||
DefaultImportRole = "delegation"
|
||||
|
||||
// HealthCheckKeyManagement and HealthCheckSigner are the grpc service name
|
||||
// for "KeyManagement" and "Signer" respectively which used for health check.
|
||||
// The "Overall" indicates the querying for overall status of the server.
|
||||
HealthCheckKeyManagement = "grpc.health.v1.Health.KeyManagement"
|
||||
HealthCheckSigner = "grpc.health.v1.Health.Signer"
|
||||
HealthCheckOverall = "grpc.health.v1.Health.Overall"
|
||||
|
||||
// PrivExecPerms indicates the file permissions for directory
|
||||
// and PrivNoExecPerms for file.
|
||||
PrivExecPerms = 0700
|
||||
PrivNoExecPerms = 0600
|
||||
|
||||
// DefaultPageSize is the default number of records to return from the changefeed
|
||||
DefaultPageSize = 100
|
||||
)
|
||||
|
||||
// NotaryDefaultExpiries is the construct used to configure the default expiry times of
|
||||
// the various role files.
|
||||
var NotaryDefaultExpiries = map[string]time.Duration{
|
||||
"root": NotaryRootExpiry,
|
||||
"targets": NotaryTargetsExpiry,
|
||||
"snapshot": NotarySnapshotExpiry,
|
||||
"timestamp": NotaryTimestampExpiry,
|
||||
// enum to use for setting and retrieving values from contexts
|
||||
const (
|
||||
CtxKeyMetaStore CtxKey = iota
|
||||
CtxKeyKeyAlgo
|
||||
CtxKeyCryptoSvc
|
||||
CtxKeyRepo
|
||||
)
|
||||
|
||||
// NotarySupportedBackends contains the backends we would like to support at present
|
||||
var NotarySupportedBackends = []string{
|
||||
MemoryBackend,
|
||||
MySQLBackend,
|
||||
SQLiteBackend,
|
||||
RethinkDBBackend,
|
||||
PostgresBackend,
|
||||
}
|
||||
|
||||
+4
-4
@@ -12,17 +12,17 @@ import (
|
||||
)
|
||||
|
||||
// GenerateCertificate generates an X509 Certificate from a template, given a GUN and validity interval
|
||||
func GenerateCertificate(rootKey data.PrivateKey, gun string, startTime, endTime time.Time) (*x509.Certificate, error) {
|
||||
func GenerateCertificate(rootKey data.PrivateKey, gun data.GUN, startTime, endTime time.Time) (*x509.Certificate, error) {
|
||||
signer := rootKey.CryptoSigner()
|
||||
if signer == nil {
|
||||
return nil, fmt.Errorf("key type not supported for Certificate generation: %s\n", rootKey.Algorithm())
|
||||
return nil, fmt.Errorf("key type not supported for Certificate generation: %s", rootKey.Algorithm())
|
||||
}
|
||||
|
||||
return generateCertificate(signer, gun, startTime, endTime)
|
||||
}
|
||||
|
||||
func generateCertificate(signer crypto.Signer, gun string, startTime, endTime time.Time) (*x509.Certificate, error) {
|
||||
template, err := utils.NewCertificate(gun, startTime, endTime)
|
||||
func generateCertificate(signer crypto.Signer, gun data.GUN, startTime, endTime time.Time) (*x509.Certificate, error) {
|
||||
template, err := utils.NewCertificate(gun.String(), startTime, endTime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create the certificate template for: %s (%v)", gun, err)
|
||||
}
|
||||
|
||||
+24
-46
@@ -1,17 +1,16 @@
|
||||
package cryptoservice
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/notary"
|
||||
"github.com/docker/notary/trustmanager"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/docker/notary/tuf/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -36,47 +35,23 @@ func NewCryptoService(keyStores ...trustmanager.KeyStore) *CryptoService {
|
||||
}
|
||||
|
||||
// Create is used to generate keys for targets, snapshots and timestamps
|
||||
func (cs *CryptoService) Create(role, gun, algorithm string) (data.PublicKey, error) {
|
||||
var privKey data.PrivateKey
|
||||
var err error
|
||||
|
||||
switch algorithm {
|
||||
case data.RSAKey:
|
||||
privKey, err = utils.GenerateRSAKey(rand.Reader, notary.MinRSABitSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate RSA key: %v", err)
|
||||
}
|
||||
case data.ECDSAKey:
|
||||
privKey, err = utils.GenerateECDSAKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate EC key: %v", err)
|
||||
}
|
||||
case data.ED25519Key:
|
||||
privKey, err = utils.GenerateED25519Key(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate ED25519 key: %v", err)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("private key type not supported for key generation: %s", algorithm)
|
||||
func (cs *CryptoService) Create(role data.RoleName, gun data.GUN, algorithm string) (data.PublicKey, error) {
|
||||
if algorithm == data.RSAKey {
|
||||
return nil, fmt.Errorf("%s keys can only be imported", data.RSAKey)
|
||||
}
|
||||
logrus.Debugf("generated new %s key for role: %s and keyID: %s", algorithm, role, privKey.ID())
|
||||
|
||||
// Store the private key into our keystore
|
||||
for _, ks := range cs.keyStores {
|
||||
err = ks.AddKey(trustmanager.KeyInfo{Role: role, Gun: gun}, privKey)
|
||||
if err == nil {
|
||||
return data.PublicKeyFromPrivate(privKey), nil
|
||||
}
|
||||
}
|
||||
privKey, err := utils.GenerateKey(algorithm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add key to filestore: %v", err)
|
||||
return nil, fmt.Errorf("failed to generate %s key: %v", algorithm, err)
|
||||
}
|
||||
logrus.Debugf("generated new %s key for role: %s and keyID: %s", algorithm, role.String(), privKey.ID())
|
||||
pubKey := data.PublicKeyFromPrivate(privKey)
|
||||
|
||||
return nil, fmt.Errorf("keystores would not accept new private keys for unknown reasons")
|
||||
return pubKey, cs.AddKey(role, gun, privKey)
|
||||
}
|
||||
|
||||
// GetPrivateKey returns a private key and role if present by ID.
|
||||
func (cs *CryptoService) GetPrivateKey(keyID string) (k data.PrivateKey, role string, err error) {
|
||||
func (cs *CryptoService) GetPrivateKey(keyID string) (k data.PrivateKey, role data.RoleName, err error) {
|
||||
for _, ks := range cs.keyStores {
|
||||
if k, role, err = ks.GetKey(keyID); err == nil {
|
||||
return
|
||||
@@ -120,14 +95,14 @@ func (cs *CryptoService) RemoveKey(keyID string) (err error) {
|
||||
|
||||
// AddKey adds a private key to a specified role.
|
||||
// The GUN is inferred from the cryptoservice itself for non-root roles
|
||||
func (cs *CryptoService) AddKey(role, gun string, key data.PrivateKey) (err error) {
|
||||
func (cs *CryptoService) AddKey(role data.RoleName, gun data.GUN, key data.PrivateKey) (err error) {
|
||||
// First check if this key already exists in any of our keystores
|
||||
for _, ks := range cs.keyStores {
|
||||
if keyInfo, err := ks.GetKeyInfo(key.ID()); err == nil {
|
||||
if keyInfo.Role != role {
|
||||
return fmt.Errorf("key with same ID already exists for role: %s", keyInfo.Role)
|
||||
return fmt.Errorf("key with same ID already exists for role: %s", keyInfo.Role.String())
|
||||
}
|
||||
logrus.Debugf("key with same ID %s and role %s already exists", key.ID(), keyInfo.Role)
|
||||
logrus.Debugf("key with same ID %s and role %s already exists", key.ID(), keyInfo.Role.String())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -142,7 +117,7 @@ func (cs *CryptoService) AddKey(role, gun string, key data.PrivateKey) (err erro
|
||||
}
|
||||
|
||||
// ListKeys returns a list of key IDs valid for the given role
|
||||
func (cs *CryptoService) ListKeys(role string) []string {
|
||||
func (cs *CryptoService) ListKeys(role data.RoleName) []string {
|
||||
var res []string
|
||||
for _, ks := range cs.keyStores {
|
||||
for k, r := range ks.ListKeys() {
|
||||
@@ -155,8 +130,8 @@ func (cs *CryptoService) ListKeys(role string) []string {
|
||||
}
|
||||
|
||||
// ListAllKeys returns a map of key IDs to role
|
||||
func (cs *CryptoService) ListAllKeys() map[string]string {
|
||||
res := make(map[string]string)
|
||||
func (cs *CryptoService) ListAllKeys() map[string]data.RoleName {
|
||||
res := make(map[string]data.RoleName)
|
||||
for _, ks := range cs.keyStores {
|
||||
for k, r := range ks.ListKeys() {
|
||||
res[k] = r.Role // keys are content addressed so don't care about overwrites
|
||||
@@ -173,9 +148,12 @@ func CheckRootKeyIsEncrypted(pemBytes []byte) error {
|
||||
return ErrNoValidPrivateKey
|
||||
}
|
||||
|
||||
if !x509.IsEncryptedPEMBlock(block) {
|
||||
return ErrRootKeyNotEncrypted
|
||||
if block.Type == "ENCRYPTED PRIVATE KEY" {
|
||||
return nil
|
||||
}
|
||||
if !notary.FIPSEnabled() && x509.IsEncryptedPEMBlock(block) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
return ErrRootKeyNotEncrypted
|
||||
}
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package notary
|
||||
|
||||
import "os"
|
||||
|
||||
// FIPSEnvVar is the name of the environment variable that is being used to switch
|
||||
// between FIPS and non-FIPS mode
|
||||
const FIPSEnvVar = "GOFIPS"
|
||||
|
||||
// FIPSEnabled returns true if environment variable `GOFIPS` has been set to enable
|
||||
// FIPS mode
|
||||
func FIPSEnabled() bool {
|
||||
return os.Getenv(FIPSEnvVar) != ""
|
||||
}
|
||||
+5
@@ -5,3 +5,8 @@ package notary
|
||||
// confirmation), createNew will be true. Attempts is passed in so that implementers
|
||||
// decide how many chances to give to a human, for example.
|
||||
type PassRetriever func(keyName, alias string, createNew bool, attempts int) (passphrase string, giveup bool, err error)
|
||||
|
||||
// CtxKey is a wrapper type for use in context.WithValue() to satisfy golint
|
||||
// https://github.com/golang/go/issues/17293
|
||||
// https://github.com/golang/lint/pull/245
|
||||
type CtxKey int
|
||||
|
||||
+22
-17
@@ -11,15 +11,13 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/pkg/term"
|
||||
"github.com/docker/notary"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
const (
|
||||
idBytesToDisplay = 7
|
||||
tufRootAlias = "root"
|
||||
tufTargetsAlias = "targets"
|
||||
tufSnapshotAlias = "snapshot"
|
||||
tufRootKeyGenerationWarning = `You are about to create a new root signing key passphrase. This passphrase
|
||||
will be used to protect the most sensitive key in your signing system. Please
|
||||
choose a long, complex passphrase and be careful to keep the password and the
|
||||
@@ -51,7 +49,7 @@ var (
|
||||
// Upon successful passphrase retrievals, the passphrase will be cached such that
|
||||
// subsequent prompts will produce the same passphrase.
|
||||
func PromptRetriever() notary.PassRetriever {
|
||||
if !term.IsTerminal(os.Stdin.Fd()) {
|
||||
if !terminal.IsTerminal(int(os.Stdin.Fd())) {
|
||||
return func(string, string, bool, int) (string, bool, error) {
|
||||
return "", false, ErrNoInput
|
||||
}
|
||||
@@ -93,17 +91,6 @@ func (br *boundRetriever) requestPassphrase(keyName, alias string, createNew boo
|
||||
displayAlias = val
|
||||
}
|
||||
|
||||
// If typing on the terminal, we do not want the terminal to echo the
|
||||
// password that is typed (so it doesn't display)
|
||||
if term.IsTerminal(os.Stdin.Fd()) {
|
||||
state, err := term.SaveState(os.Stdin.Fd())
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
term.DisableEcho(os.Stdin.Fd(), state)
|
||||
defer term.RestoreTerminal(os.Stdin.Fd(), state)
|
||||
}
|
||||
|
||||
indexOfLastSeparator := strings.LastIndex(keyName, string(filepath.Separator))
|
||||
if indexOfLastSeparator == -1 {
|
||||
indexOfLastSeparator = 0
|
||||
@@ -135,7 +122,7 @@ func (br *boundRetriever) requestPassphrase(keyName, alias string, createNew boo
|
||||
}
|
||||
|
||||
stdin := bufio.NewReader(br.in)
|
||||
passphrase, err := stdin.ReadBytes('\n')
|
||||
passphrase, err := GetPassphrase(stdin)
|
||||
fmt.Fprintln(br.out)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
@@ -162,7 +149,8 @@ func (br *boundRetriever) verifyAndConfirmPassword(stdin *bufio.Reader, retPass,
|
||||
}
|
||||
|
||||
fmt.Fprintf(br.out, "Repeat passphrase for new %s key%s: ", displayAlias, withID)
|
||||
confirmation, err := stdin.ReadBytes('\n')
|
||||
|
||||
confirmation, err := GetPassphrase(stdin)
|
||||
fmt.Fprintln(br.out)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -203,3 +191,20 @@ func ConstantRetriever(constantPassphrase string) notary.PassRetriever {
|
||||
return constantPassphrase, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetPassphrase get the passphrase from bufio.Reader or from terminal.
|
||||
// If typing on the terminal, we disable terminal to echo the passphrase.
|
||||
func GetPassphrase(in *bufio.Reader) ([]byte, error) {
|
||||
var (
|
||||
passphrase []byte
|
||||
err error
|
||||
)
|
||||
|
||||
if terminal.IsTerminal(int(os.Stdin.Fd())) {
|
||||
passphrase, err = terminal.ReadPassword(int(os.Stdin.Fd()))
|
||||
} else {
|
||||
passphrase, err = in.ReadBytes('\n')
|
||||
}
|
||||
|
||||
return passphrase, err
|
||||
}
|
||||
|
||||
+79
-22
@@ -1,6 +1,8 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -9,19 +11,13 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/notary"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// NewFilesystemStore creates a new store in a directory tree
|
||||
func NewFilesystemStore(baseDir, subDir, extension string) (*FilesystemStore, error) {
|
||||
baseDir = filepath.Join(baseDir, subDir)
|
||||
|
||||
return NewFileStore(baseDir, extension, notary.PrivKeyPerms)
|
||||
}
|
||||
|
||||
// NewFileStore creates a fully configurable file store
|
||||
func NewFileStore(baseDir, fileExt string, perms os.FileMode) (*FilesystemStore, error) {
|
||||
func NewFileStore(baseDir, fileExt string) (*FilesystemStore, error) {
|
||||
baseDir = filepath.Clean(baseDir)
|
||||
if err := createDirectory(baseDir, perms); err != nil {
|
||||
if err := createDirectory(baseDir, notary.PrivExecPerms); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !strings.HasPrefix(fileExt, ".") {
|
||||
@@ -31,34 +27,95 @@ func NewFileStore(baseDir, fileExt string, perms os.FileMode) (*FilesystemStore,
|
||||
return &FilesystemStore{
|
||||
baseDir: baseDir,
|
||||
ext: fileExt,
|
||||
perms: perms,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewSimpleFileStore is a convenience wrapper to create a world readable,
|
||||
// owner writeable filestore
|
||||
func NewSimpleFileStore(baseDir, fileExt string) (*FilesystemStore, error) {
|
||||
return NewFileStore(baseDir, fileExt, notary.PubCertPerms)
|
||||
}
|
||||
|
||||
// NewPrivateKeyFileStorage initializes a new filestore for private keys, appending
|
||||
// the notary.PrivDir to the baseDir.
|
||||
func NewPrivateKeyFileStorage(baseDir, fileExt string) (*FilesystemStore, error) {
|
||||
baseDir = filepath.Join(baseDir, notary.PrivDir)
|
||||
return NewFileStore(baseDir, fileExt, notary.PrivKeyPerms)
|
||||
myStore, err := NewFileStore(baseDir, fileExt)
|
||||
myStore.migrateTo0Dot4()
|
||||
return myStore, err
|
||||
}
|
||||
|
||||
// NewPrivateSimpleFileStore is a wrapper to create an owner readable/writeable
|
||||
// _only_ filestore
|
||||
func NewPrivateSimpleFileStore(baseDir, fileExt string) (*FilesystemStore, error) {
|
||||
return NewFileStore(baseDir, fileExt, notary.PrivKeyPerms)
|
||||
return NewFileStore(baseDir, fileExt)
|
||||
}
|
||||
|
||||
// FilesystemStore is a store in a locally accessible directory
|
||||
type FilesystemStore struct {
|
||||
baseDir string
|
||||
ext string
|
||||
perms os.FileMode
|
||||
}
|
||||
|
||||
func (f *FilesystemStore) moveKeyTo0Dot4Location(file string) {
|
||||
keyID := filepath.Base(file)
|
||||
fileDir := filepath.Dir(file)
|
||||
d, _ := f.Get(file)
|
||||
block, _ := pem.Decode(d)
|
||||
if block == nil {
|
||||
logrus.Warn("Key data for", file, "could not be decoded as a valid PEM block. The key will not been migrated and may not be available")
|
||||
return
|
||||
}
|
||||
fileDir = strings.TrimPrefix(fileDir, notary.RootKeysSubdir)
|
||||
fileDir = strings.TrimPrefix(fileDir, notary.NonRootKeysSubdir)
|
||||
if fileDir != "" {
|
||||
block.Headers["gun"] = filepath.ToSlash(fileDir[1:])
|
||||
}
|
||||
if strings.Contains(keyID, "_") {
|
||||
role := strings.Split(keyID, "_")[1]
|
||||
keyID = strings.TrimSuffix(keyID, "_"+role)
|
||||
block.Headers["role"] = role
|
||||
}
|
||||
var keyPEM bytes.Buffer
|
||||
// since block came from decoding the PEM bytes in the first place, and all we're doing is adding some headers we ignore the possibility of an error while encoding the block
|
||||
pem.Encode(&keyPEM, block)
|
||||
f.Set(keyID, keyPEM.Bytes())
|
||||
}
|
||||
|
||||
func (f *FilesystemStore) migrateTo0Dot4() {
|
||||
rootKeysSubDir := filepath.Clean(filepath.Join(f.Location(), notary.RootKeysSubdir))
|
||||
nonRootKeysSubDir := filepath.Clean(filepath.Join(f.Location(), notary.NonRootKeysSubdir))
|
||||
if _, err := os.Stat(rootKeysSubDir); !os.IsNotExist(err) && f.Location() != rootKeysSubDir {
|
||||
if rootKeysSubDir == "" || rootKeysSubDir == "/" {
|
||||
// making sure we don't remove a user's homedir
|
||||
logrus.Warn("The directory for root keys is an unsafe value, we are not going to delete the directory. Please delete it manually")
|
||||
} else {
|
||||
// root_keys exists, migrate things from it
|
||||
listOnlyRootKeysDirStore, _ := NewFileStore(rootKeysSubDir, f.ext)
|
||||
for _, file := range listOnlyRootKeysDirStore.ListFiles() {
|
||||
f.moveKeyTo0Dot4Location(filepath.Join(notary.RootKeysSubdir, file))
|
||||
}
|
||||
// delete the old directory
|
||||
os.RemoveAll(rootKeysSubDir)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(nonRootKeysSubDir); !os.IsNotExist(err) && f.Location() != nonRootKeysSubDir {
|
||||
if nonRootKeysSubDir == "" || nonRootKeysSubDir == "/" {
|
||||
// making sure we don't remove a user's homedir
|
||||
logrus.Warn("The directory for non root keys is an unsafe value, we are not going to delete the directory. Please delete it manually")
|
||||
} else {
|
||||
// tuf_keys exists, migrate things from it
|
||||
listOnlyNonRootKeysDirStore, _ := NewFileStore(nonRootKeysSubDir, f.ext)
|
||||
for _, file := range listOnlyNonRootKeysDirStore.ListFiles() {
|
||||
f.moveKeyTo0Dot4Location(filepath.Join(notary.NonRootKeysSubdir, file))
|
||||
}
|
||||
// delete the old directory
|
||||
os.RemoveAll(nonRootKeysSubDir)
|
||||
}
|
||||
}
|
||||
|
||||
// if we have a trusted_certificates folder, let's delete for a complete migration since it is unused by new clients
|
||||
certsSubDir := filepath.Join(f.Location(), "trusted_certificates")
|
||||
if certsSubDir == "" || certsSubDir == "/" {
|
||||
logrus.Warn("The directory for trusted certificate is an unsafe value, we are not going to delete the directory. Please delete it manually")
|
||||
} else {
|
||||
os.RemoveAll(certsSubDir)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FilesystemStore) getPath(name string) (string, error) {
|
||||
@@ -80,7 +137,7 @@ func (f *FilesystemStore) GetSized(name string, size int64) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file, err := os.OpenFile(p, os.O_RDONLY, f.perms)
|
||||
file, err := os.OpenFile(p, os.O_RDONLY, notary.PrivNoExecPerms)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = ErrMetaNotFound{Resource: name}
|
||||
@@ -140,7 +197,7 @@ func (f *FilesystemStore) Set(name string, meta []byte) error {
|
||||
}
|
||||
|
||||
// Ensures the parent directories of the file we are about to write exist
|
||||
err = os.MkdirAll(filepath.Dir(fp), f.perms)
|
||||
err = os.MkdirAll(filepath.Dir(fp), notary.PrivExecPerms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -149,7 +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, f.perms); err != nil {
|
||||
if err = ioutil.WriteFile(fp, meta, notary.PrivNoExecPerms); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
+39
-11
@@ -22,9 +22,17 @@ import (
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/docker/notary"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/docker/notary/tuf/validation"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxErrorResponseSize is the maximum size for an error message - 1KiB
|
||||
MaxErrorResponseSize int64 = 1 << 10
|
||||
// MaxKeySize is the maximum size for a stored TUF key - 256KiB
|
||||
MaxKeySize = 256 << 10
|
||||
)
|
||||
|
||||
// ErrServerUnavailable indicates an error from the server. code allows us to
|
||||
@@ -39,6 +47,21 @@ type NetworkError struct {
|
||||
}
|
||||
|
||||
func (n NetworkError) Error() string {
|
||||
if _, ok := n.Wrapped.(*url.Error); ok {
|
||||
// QueryUnescape does the inverse transformation of QueryEscape,
|
||||
// converting %AB into the byte 0xAB and '+' into ' ' (space).
|
||||
// It returns an error if any % is not followed by two hexadecimal digits.
|
||||
//
|
||||
// If this happens, we log out the QueryUnescape error and return the
|
||||
// original error to client.
|
||||
res, err := url.QueryUnescape(n.Wrapped.Error())
|
||||
if err != nil {
|
||||
logrus.Errorf("unescape network error message failed: %s", err)
|
||||
return n.Wrapped.Error()
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
return n.Wrapped.Error()
|
||||
}
|
||||
|
||||
@@ -88,7 +111,9 @@ type HTTPStore struct {
|
||||
roundTrip http.RoundTripper
|
||||
}
|
||||
|
||||
// NewHTTPStore initializes a new store against a URL and a number of configuration options
|
||||
// NewHTTPStore initializes a new store against a URL and a number of configuration options.
|
||||
//
|
||||
// In case of a nil `roundTrip`, a default offline store is used instead.
|
||||
func NewHTTPStore(baseURL, metaPrefix, metaExtension, keyExtension string, roundTrip http.RoundTripper) (RemoteStore, error) {
|
||||
base, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
@@ -110,7 +135,8 @@ func NewHTTPStore(baseURL, metaPrefix, metaExtension, keyExtension string, round
|
||||
}
|
||||
|
||||
func tryUnmarshalError(resp *http.Response, defaultError error) error {
|
||||
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
||||
b := io.LimitReader(resp.Body, MaxErrorResponseSize)
|
||||
bodyBytes, err := ioutil.ReadAll(b)
|
||||
if err != nil {
|
||||
return defaultError
|
||||
}
|
||||
@@ -269,8 +295,8 @@ func (s HTTPStore) buildMetaURL(name string) (*url.URL, error) {
|
||||
return s.buildURL(uri)
|
||||
}
|
||||
|
||||
func (s HTTPStore) buildKeyURL(name string) (*url.URL, error) {
|
||||
filename := fmt.Sprintf("%s.%s", name, s.keyExtension)
|
||||
func (s HTTPStore) buildKeyURL(name data.RoleName) (*url.URL, error) {
|
||||
filename := fmt.Sprintf("%s.%s", name.String(), s.keyExtension)
|
||||
uri := path.Join(s.metaPrefix, filename)
|
||||
return s.buildURL(uri)
|
||||
}
|
||||
@@ -284,7 +310,7 @@ func (s HTTPStore) buildURL(uri string) (*url.URL, error) {
|
||||
}
|
||||
|
||||
// GetKey retrieves a public key from the remote server
|
||||
func (s HTTPStore) GetKey(role string) ([]byte, error) {
|
||||
func (s HTTPStore) GetKey(role data.RoleName) ([]byte, error) {
|
||||
url, err := s.buildKeyURL(role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -298,10 +324,11 @@ func (s HTTPStore) GetKey(role string) ([]byte, error) {
|
||||
return nil, NetworkError{Wrapped: err}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if err := translateStatusToError(resp, role+" key"); err != nil {
|
||||
if err := translateStatusToError(resp, role.String()+" key"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
b := io.LimitReader(resp.Body, MaxKeySize)
|
||||
body, err := ioutil.ReadAll(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -309,7 +336,7 @@ func (s HTTPStore) GetKey(role string) ([]byte, error) {
|
||||
}
|
||||
|
||||
// RotateKey rotates a private key and returns the public component from the remote server
|
||||
func (s HTTPStore) RotateKey(role string) ([]byte, error) {
|
||||
func (s HTTPStore) RotateKey(role data.RoleName) ([]byte, error) {
|
||||
url, err := s.buildKeyURL(role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -323,10 +350,11 @@ func (s HTTPStore) RotateKey(role string) ([]byte, error) {
|
||||
return nil, NetworkError{Wrapped: err}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if err := translateStatusToError(resp, role+" key"); err != nil {
|
||||
if err := translateStatusToError(resp, role.String()+" key"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
b := io.LimitReader(resp.Body, MaxKeySize)
|
||||
body, err := ioutil.ReadAll(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+6
-2
@@ -1,5 +1,9 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"github.com/docker/notary/tuf/data"
|
||||
)
|
||||
|
||||
// NoSizeLimit is represented as -1 for arguments to GetMeta
|
||||
const NoSizeLimit int64 = -1
|
||||
|
||||
@@ -15,8 +19,8 @@ type MetadataStore interface {
|
||||
|
||||
// PublicKeyStore must be implemented by a key service
|
||||
type PublicKeyStore interface {
|
||||
GetKey(role string) ([]byte, error)
|
||||
RotateKey(role string) ([]byte, error)
|
||||
GetKey(role data.RoleName) ([]byte, error)
|
||||
RotateKey(role data.RoleName) ([]byte, error)
|
||||
}
|
||||
|
||||
// RemoteStore is similar to LocalStore with the added expectation that it should
|
||||
|
||||
+24
-11
@@ -2,25 +2,29 @@ package storage
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/notary"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/docker/notary/tuf/utils"
|
||||
)
|
||||
|
||||
// NewMemoryStore returns a MetadataStore that operates entirely in memory.
|
||||
// Very useful for testing
|
||||
func NewMemoryStore(initial map[string][]byte) *MemoryStore {
|
||||
var consistent = make(map[string][]byte)
|
||||
if initial == nil {
|
||||
initial = make(map[string][]byte)
|
||||
} else {
|
||||
// add all seed meta to consistent
|
||||
for name, data := range initial {
|
||||
checksum := sha256.Sum256(data)
|
||||
path := utils.ConsistentName(name, checksum[:])
|
||||
consistent[path] = data
|
||||
}
|
||||
func NewMemoryStore(seed map[data.RoleName][]byte) *MemoryStore {
|
||||
var (
|
||||
consistent = make(map[string][]byte)
|
||||
initial = make(map[string][]byte)
|
||||
)
|
||||
// add all seed meta to consistent
|
||||
for name, d := range seed {
|
||||
checksum := sha256.Sum256(d)
|
||||
path := utils.ConsistentName(name.String(), checksum[:])
|
||||
initial[name.String()] = d
|
||||
consistent[path] = d
|
||||
}
|
||||
|
||||
return &MemoryStore{
|
||||
data: initial,
|
||||
consistent: consistent,
|
||||
@@ -75,6 +79,15 @@ func (m MemoryStore) Get(name string) ([]byte, error) {
|
||||
func (m *MemoryStore) Set(name string, meta []byte) error {
|
||||
m.data[name] = meta
|
||||
|
||||
parsedMeta := &data.SignedMeta{}
|
||||
err := json.Unmarshal(meta, parsedMeta)
|
||||
if err == nil {
|
||||
// no parse error means this is metadata and not a key, so store by version
|
||||
version := parsedMeta.Signed.Version
|
||||
versionedName := fmt.Sprintf("%d.%s", version, name)
|
||||
m.data[versionedName] = meta
|
||||
}
|
||||
|
||||
checksum := sha256.Sum256(meta)
|
||||
path := utils.ConsistentName(name, checksum[:])
|
||||
m.consistent[path] = meta
|
||||
|
||||
+6
-2
@@ -1,5 +1,9 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"github.com/docker/notary/tuf/data"
|
||||
)
|
||||
|
||||
// ErrOffline is used to indicate we are operating offline
|
||||
type ErrOffline struct{}
|
||||
|
||||
@@ -34,12 +38,12 @@ func (es OfflineStore) Remove(name string) error {
|
||||
}
|
||||
|
||||
// GetKey returns ErrOffline
|
||||
func (es OfflineStore) GetKey(role string) ([]byte, error) {
|
||||
func (es OfflineStore) GetKey(role data.RoleName) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// RotateKey returns ErrOffline
|
||||
func (es OfflineStore) RotateKey(role string) ([]byte, error) {
|
||||
func (es OfflineStore) RotateKey(role data.RoleName) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package trustmanager
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ErrAttemptsExceeded is returned when too many attempts have been made to decrypt a key
|
||||
type ErrAttemptsExceeded struct{}
|
||||
|
||||
// ErrAttemptsExceeded is returned when too many attempts have been made to decrypt a key
|
||||
func (err ErrAttemptsExceeded) Error() string {
|
||||
return "maximum number of passphrase attempts exceeded"
|
||||
}
|
||||
|
||||
// ErrPasswordInvalid is returned when signing fails. It could also mean the signing
|
||||
// key file was corrupted, but we have no way to distinguish.
|
||||
type ErrPasswordInvalid struct{}
|
||||
|
||||
// ErrPasswordInvalid is returned when signing fails. It could also mean the signing
|
||||
// key file was corrupted, but we have no way to distinguish.
|
||||
func (err ErrPasswordInvalid) Error() string {
|
||||
return "password invalid, operation has failed."
|
||||
}
|
||||
|
||||
// ErrKeyNotFound is returned when the keystore fails to retrieve a specific key.
|
||||
type ErrKeyNotFound struct {
|
||||
KeyID string
|
||||
}
|
||||
|
||||
// ErrKeyNotFound is returned when the keystore fails to retrieve a specific key.
|
||||
func (err ErrKeyNotFound) Error() string {
|
||||
return fmt.Sprintf("signing key not found: %s", err.KeyID)
|
||||
}
|
||||
+6
-34
@@ -1,8 +1,6 @@
|
||||
package trustmanager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/notary/tuf/data"
|
||||
)
|
||||
|
||||
@@ -34,32 +32,11 @@ type Storage interface {
|
||||
Location() string
|
||||
}
|
||||
|
||||
// ErrAttemptsExceeded is returned when too many attempts have been made to decrypt a key
|
||||
type ErrAttemptsExceeded struct{}
|
||||
|
||||
// ErrAttemptsExceeded is returned when too many attempts have been made to decrypt a key
|
||||
func (err ErrAttemptsExceeded) Error() string {
|
||||
return "maximum number of passphrase attempts exceeded"
|
||||
}
|
||||
|
||||
// ErrPasswordInvalid is returned when signing fails. It could also mean the signing
|
||||
// key file was corrupted, but we have no way to distinguish.
|
||||
type ErrPasswordInvalid struct{}
|
||||
|
||||
// ErrPasswordInvalid is returned when signing fails. It could also mean the signing
|
||||
// key file was corrupted, but we have no way to distinguish.
|
||||
func (err ErrPasswordInvalid) Error() string {
|
||||
return "password invalid, operation has failed."
|
||||
}
|
||||
|
||||
// ErrKeyNotFound is returned when the keystore fails to retrieve a specific key.
|
||||
type ErrKeyNotFound struct {
|
||||
KeyID string
|
||||
}
|
||||
|
||||
// ErrKeyNotFound is returned when the keystore fails to retrieve a specific key.
|
||||
func (err ErrKeyNotFound) Error() string {
|
||||
return fmt.Sprintf("signing key not found: %s", err.KeyID)
|
||||
// KeyInfo stores the role and gun for a corresponding private key ID
|
||||
// It is assumed that each private key ID is unique
|
||||
type KeyInfo struct {
|
||||
Gun data.GUN
|
||||
Role data.RoleName
|
||||
}
|
||||
|
||||
// KeyStore is a generic interface for private key storage
|
||||
@@ -69,14 +46,9 @@ type KeyStore interface {
|
||||
AddKey(keyInfo KeyInfo, privKey data.PrivateKey) error
|
||||
// Should fail with ErrKeyNotFound if the keystore is operating normally
|
||||
// and knows that it does not store the requested key.
|
||||
GetKey(keyID string) (data.PrivateKey, string, error)
|
||||
GetKey(keyID string) (data.PrivateKey, data.RoleName, error)
|
||||
GetKeyInfo(keyID string) (KeyInfo, error)
|
||||
ListKeys() map[string]KeyInfo
|
||||
RemoveKey(keyID string) error
|
||||
Name() string
|
||||
}
|
||||
|
||||
type cachedKey struct {
|
||||
alias string
|
||||
key data.PrivateKey
|
||||
}
|
||||
|
||||
+40
-103
@@ -1,26 +1,23 @@
|
||||
package trustmanager
|
||||
|
||||
import (
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/docker/notary"
|
||||
store "github.com/docker/notary/storage"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/docker/notary/tuf/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type keyInfoMap map[string]KeyInfo
|
||||
|
||||
// KeyInfo stores the role, path, and gun for a corresponding private key ID
|
||||
// It is assumed that each private key ID is unique
|
||||
type KeyInfo struct {
|
||||
Gun string
|
||||
Role string
|
||||
type cachedKey struct {
|
||||
role data.RoleName
|
||||
key data.PrivateKey
|
||||
}
|
||||
|
||||
// GenericKeyStore is a wrapper for Storage instances that provides
|
||||
@@ -80,40 +77,6 @@ func generateKeyInfoMap(s Storage) map[string]KeyInfo {
|
||||
return keyInfoMap
|
||||
}
|
||||
|
||||
// Attempts to infer the keyID, role, and GUN from the specified key path.
|
||||
// Note that non-root roles can only be inferred if this is a legacy style filename: KEYID_ROLE.key
|
||||
func inferKeyInfoFromKeyPath(keyPath string) (string, string, string) {
|
||||
var keyID, role, gun string
|
||||
keyID = filepath.Base(keyPath)
|
||||
underscoreIndex := strings.LastIndex(keyID, "_")
|
||||
|
||||
// This is the legacy KEYID_ROLE filename
|
||||
// The keyID is the first part of the keyname
|
||||
// The keyRole is the second part of the keyname
|
||||
// in a key named abcde_root, abcde is the keyID and root is the KeyAlias
|
||||
if underscoreIndex != -1 {
|
||||
role = keyID[underscoreIndex+1:]
|
||||
keyID = keyID[:underscoreIndex]
|
||||
}
|
||||
|
||||
if filepath.HasPrefix(keyPath, notary.RootKeysSubdir+"/") {
|
||||
return keyID, data.CanonicalRootRole, ""
|
||||
}
|
||||
|
||||
keyPath = strings.TrimPrefix(keyPath, notary.NonRootKeysSubdir+"/")
|
||||
gun = getGunFromFullID(keyPath)
|
||||
return keyID, role, gun
|
||||
}
|
||||
|
||||
func getGunFromFullID(fullKeyID string) string {
|
||||
keyGun := filepath.Dir(fullKeyID)
|
||||
// If the gun is empty, Dir will return .
|
||||
if keyGun == "." {
|
||||
keyGun = ""
|
||||
}
|
||||
return keyGun
|
||||
}
|
||||
|
||||
func (s *GenericKeyStore) loadKeyInfo() {
|
||||
s.keyInfoMap = generateKeyInfoMap(s.store)
|
||||
}
|
||||
@@ -139,9 +102,9 @@ func (s *GenericKeyStore) AddKey(keyInfo KeyInfo, privKey data.PrivateKey) error
|
||||
if keyInfo.Role == data.CanonicalRootRole || data.IsDelegation(keyInfo.Role) || !data.ValidRole(keyInfo.Role) {
|
||||
keyInfo.Gun = ""
|
||||
}
|
||||
name := filepath.Join(keyInfo.Gun, privKey.ID())
|
||||
keyID := privKey.ID()
|
||||
for attempts := 0; ; attempts++ {
|
||||
chosenPassphrase, giveup, err = s.PassRetriever(name, keyInfo.Role, true, attempts)
|
||||
chosenPassphrase, giveup, err = s.PassRetriever(keyID, keyInfo.Role.String(), true, attempts)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
@@ -150,18 +113,14 @@ func (s *GenericKeyStore) AddKey(keyInfo KeyInfo, privKey data.PrivateKey) error
|
||||
}
|
||||
}
|
||||
|
||||
if chosenPassphrase != "" {
|
||||
pemPrivKey, err = utils.EncryptPrivateKey(privKey, keyInfo.Role, keyInfo.Gun, chosenPassphrase)
|
||||
} else {
|
||||
pemPrivKey, err = utils.KeyToPEM(privKey, keyInfo.Role)
|
||||
}
|
||||
pemPrivKey, err = utils.ConvertPrivateKeyToPKCS8(privKey, keyInfo.Role, keyInfo.Gun, chosenPassphrase)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.cachedKeys[name] = &cachedKey{alias: keyInfo.Role, key: privKey}
|
||||
err = s.store.Set(filepath.Join(getSubdir(keyInfo.Role), name), pemPrivKey)
|
||||
s.cachedKeys[keyID] = &cachedKey{role: keyInfo.Role, key: privKey}
|
||||
err = s.store.Set(keyID, pemPrivKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -170,15 +129,21 @@ func (s *GenericKeyStore) AddKey(keyInfo KeyInfo, privKey data.PrivateKey) error
|
||||
}
|
||||
|
||||
// GetKey returns the PrivateKey given a KeyID
|
||||
func (s *GenericKeyStore) GetKey(name string) (data.PrivateKey, string, error) {
|
||||
func (s *GenericKeyStore) GetKey(keyID string) (data.PrivateKey, data.RoleName, error) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
cachedKeyEntry, ok := s.cachedKeys[name]
|
||||
|
||||
cachedKeyEntry, ok := s.cachedKeys[keyID]
|
||||
if ok {
|
||||
return cachedKeyEntry.key, cachedKeyEntry.alias, nil
|
||||
return cachedKeyEntry.key, cachedKeyEntry.role, nil
|
||||
}
|
||||
|
||||
keyBytes, _, keyAlias, err := getKey(s.store, name)
|
||||
role, err := getKeyRole(s.store, keyID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
keyBytes, err := s.store.Get(keyID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
@@ -186,13 +151,13 @@ func (s *GenericKeyStore) GetKey(name string) (data.PrivateKey, string, error) {
|
||||
// See if the key is encrypted. If its encrypted we'll fail to parse the private key
|
||||
privKey, err := utils.ParsePEMPrivateKey(keyBytes, "")
|
||||
if err != nil {
|
||||
privKey, _, err = GetPasswdDecryptBytes(s.PassRetriever, keyBytes, name, string(keyAlias))
|
||||
privKey, _, err = GetPasswdDecryptBytes(s.PassRetriever, keyBytes, keyID, string(role))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
s.cachedKeys[name] = &cachedKey{alias: keyAlias, key: privKey}
|
||||
return privKey, keyAlias, nil
|
||||
s.cachedKeys[keyID] = &cachedKey{role: role, key: privKey}
|
||||
return privKey, role, nil
|
||||
}
|
||||
|
||||
// ListKeys returns a list of unique PublicKeys present on the KeyFileStore, by returning a copy of the keyInfoMap
|
||||
@@ -204,24 +169,14 @@ func (s *GenericKeyStore) ListKeys() map[string]KeyInfo {
|
||||
func (s *GenericKeyStore) RemoveKey(keyID string) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
_, filename, _, err := getKey(s.store, keyID)
|
||||
switch err.(type) {
|
||||
case ErrKeyNotFound, nil:
|
||||
break
|
||||
default:
|
||||
return err
|
||||
}
|
||||
|
||||
delete(s.cachedKeys, keyID)
|
||||
|
||||
err = s.store.Remove(filename) // removing a file that doesn't exist doesn't fail
|
||||
err := s.store.Remove(keyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove this key from our keyInfo map if we removed from our filesystem
|
||||
delete(s.keyInfoMap, filepath.Base(keyID))
|
||||
delete(s.keyInfoMap, keyID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -242,55 +197,37 @@ func copyKeyInfoMap(keyInfoMap map[string]KeyInfo) map[string]KeyInfo {
|
||||
|
||||
// KeyInfoFromPEM attempts to get a keyID and KeyInfo from the filename and PEM bytes of a key
|
||||
func KeyInfoFromPEM(pemBytes []byte, filename string) (string, KeyInfo, error) {
|
||||
keyID, role, gun := inferKeyInfoFromKeyPath(filename)
|
||||
if role == "" {
|
||||
block, _ := pem.Decode(pemBytes)
|
||||
if block == nil {
|
||||
return "", KeyInfo{}, fmt.Errorf("could not decode PEM block for key %s", filename)
|
||||
}
|
||||
if keyRole, ok := block.Headers["role"]; ok {
|
||||
role = keyRole
|
||||
}
|
||||
var keyID string
|
||||
keyID = filepath.Base(filename)
|
||||
role, gun, err := utils.ExtractPrivateKeyAttributes(pemBytes)
|
||||
if err != nil {
|
||||
return "", KeyInfo{}, err
|
||||
}
|
||||
return keyID, KeyInfo{Gun: gun, Role: role}, nil
|
||||
}
|
||||
|
||||
// getKey finds the key and role for the given keyID. It attempts to
|
||||
// look both in the newer format PEM headers, and also in the legacy filename
|
||||
// format. It returns: the key bytes, the filename it was found under, the role,
|
||||
// and an error
|
||||
func getKey(s Storage, keyID string) ([]byte, string, string, error) {
|
||||
// getKeyRole finds the role for the given keyID. It attempts to look
|
||||
// both in the newer format PEM headers, and also in the legacy filename
|
||||
// format. It returns: the role, and an error
|
||||
func getKeyRole(s Storage, keyID string) (data.RoleName, error) {
|
||||
name := strings.TrimSpace(strings.TrimSuffix(filepath.Base(keyID), filepath.Ext(keyID)))
|
||||
|
||||
for _, file := range s.ListFiles() {
|
||||
filename := filepath.Base(file)
|
||||
|
||||
if strings.HasPrefix(filename, name) {
|
||||
d, err := s.Get(file)
|
||||
if err != nil {
|
||||
return nil, "", "", err
|
||||
}
|
||||
block, _ := pem.Decode(d)
|
||||
if block != nil {
|
||||
if role, ok := block.Headers["role"]; ok {
|
||||
return d, file, role, nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
role := strings.TrimPrefix(filename, name+"_")
|
||||
return d, file, role, nil
|
||||
role, _, err := utils.ExtractPrivateKeyAttributes(d)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return role, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, "", "", ErrKeyNotFound{KeyID: keyID}
|
||||
}
|
||||
|
||||
// Assumes 2 subdirectories, 1 containing root keys and 1 containing TUF keys
|
||||
func getSubdir(alias string) string {
|
||||
if alias == data.CanonicalRootRole {
|
||||
return notary.RootKeysSubdir
|
||||
}
|
||||
return notary.NonRootKeysSubdir
|
||||
return "", ErrKeyNotFound{KeyID: keyID}
|
||||
}
|
||||
|
||||
// GetPasswdDecryptBytes gets the password to decrypt the given pem bytes.
|
||||
|
||||
+4
-2
@@ -5,8 +5,10 @@ package yubikey
|
||||
import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
|
||||
"github.com/docker/notary"
|
||||
"github.com/docker/notary/trustmanager"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/docker/notary/tuf/utils"
|
||||
)
|
||||
|
||||
@@ -39,7 +41,7 @@ func (s *YubiImport) Set(name string, bytes []byte) error {
|
||||
}
|
||||
ki := trustmanager.KeyInfo{
|
||||
// GUN is ignored by YubiStore
|
||||
Role: role,
|
||||
Role: data.RoleName(role),
|
||||
}
|
||||
privKey, err := utils.ParsePEMPrivateKey(bytes, "")
|
||||
if err != nil {
|
||||
@@ -47,7 +49,7 @@ func (s *YubiImport) Set(name string, bytes []byte) error {
|
||||
s.passRetriever,
|
||||
bytes,
|
||||
name,
|
||||
ki.Role,
|
||||
ki.Role.String(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
Generated
Vendored
+51
-41
@@ -16,13 +16,13 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/docker/notary"
|
||||
"github.com/docker/notary/trustmanager"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/docker/notary/tuf/signed"
|
||||
"github.com/docker/notary/tuf/utils"
|
||||
"github.com/miekg/pkcs11"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -126,7 +126,7 @@ func (err errHSMNotPresent) Error() string {
|
||||
}
|
||||
|
||||
type yubiSlot struct {
|
||||
role string
|
||||
role data.RoleName
|
||||
slotID []byte
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ func (y *YubiPrivateKey) Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts
|
||||
return sig, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("Failed to generate signature on Yubikey.")
|
||||
return nil, errors.New("failed to generate signature on Yubikey")
|
||||
}
|
||||
|
||||
// If a byte array is less than the number of bytes specified by
|
||||
@@ -230,7 +230,7 @@ func addECDSAKey(
|
||||
privKey data.PrivateKey,
|
||||
pkcs11KeyID []byte,
|
||||
passRetriever notary.PassRetriever,
|
||||
role string,
|
||||
role data.RoleName,
|
||||
) error {
|
||||
logrus.Debugf("Attempting to add key to yubikey with ID: %s", privKey.ID())
|
||||
|
||||
@@ -250,7 +250,7 @@ func addECDSAKey(
|
||||
|
||||
// Hard-coded policy: the generated certificate expires in 10 years.
|
||||
startTime := time.Now()
|
||||
template, err := utils.NewCertificate(role, startTime, startTime.AddDate(10, 0, 0))
|
||||
template, err := utils.NewCertificate(role.String(), startTime, startTime.AddDate(10, 0, 0))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create the certificate template: %v", err)
|
||||
}
|
||||
@@ -288,7 +288,7 @@ func addECDSAKey(
|
||||
return nil
|
||||
}
|
||||
|
||||
func getECDSAKey(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte) (*data.ECDSAPublicKey, string, error) {
|
||||
func getECDSAKey(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte) (*data.ECDSAPublicKey, data.RoleName, error) {
|
||||
findTemplate := []*pkcs11.Attribute{
|
||||
pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID),
|
||||
@@ -448,45 +448,19 @@ func yubiRemoveKey(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []b
|
||||
|
||||
func yubiListKeys(ctx IPKCS11Ctx, session pkcs11.SessionHandle) (keys map[string]yubiSlot, err error) {
|
||||
keys = make(map[string]yubiSlot)
|
||||
findTemplate := []*pkcs11.Attribute{
|
||||
pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true),
|
||||
//pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_CERTIFICATE),
|
||||
}
|
||||
|
||||
attrTemplate := []*pkcs11.Attribute{
|
||||
pkcs11.NewAttribute(pkcs11.CKA_ID, []byte{0}),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_VALUE, []byte{0}),
|
||||
}
|
||||
|
||||
if err = ctx.FindObjectsInit(session, findTemplate); err != nil {
|
||||
logrus.Debugf("Failed to init: %s", err.Error())
|
||||
return
|
||||
}
|
||||
objs, b, err := ctx.FindObjects(session, numSlots)
|
||||
for err == nil {
|
||||
var o []pkcs11.ObjectHandle
|
||||
o, b, err = ctx.FindObjects(session, numSlots)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if len(o) == 0 {
|
||||
break
|
||||
}
|
||||
objs = append(objs, o...)
|
||||
}
|
||||
objs, err := listObjects(ctx, session)
|
||||
if err != nil {
|
||||
logrus.Debugf("Failed to find: %s %v", err.Error(), b)
|
||||
if len(objs) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err = ctx.FindObjectsFinal(session); err != nil {
|
||||
logrus.Debugf("Failed to finalize: %s", err.Error())
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(objs) == 0 {
|
||||
return nil, errors.New("No keys found in yubikey.")
|
||||
return nil, errors.New("no keys found in yubikey")
|
||||
}
|
||||
logrus.Debugf("Found %d objects matching list filters", len(objs))
|
||||
for _, obj := range objs {
|
||||
@@ -511,7 +485,7 @@ func yubiListKeys(ctx IPKCS11Ctx, session pkcs11.SessionHandle) (keys map[string
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !data.ValidRole(cert.Subject.CommonName) {
|
||||
if !data.ValidRole(data.RoleName(cert.Subject.CommonName)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -538,13 +512,49 @@ func yubiListKeys(ctx IPKCS11Ctx, session pkcs11.SessionHandle) (keys map[string
|
||||
}
|
||||
|
||||
keys[data.NewECDSAPublicKey(pubBytes).ID()] = yubiSlot{
|
||||
role: cert.Subject.CommonName,
|
||||
role: data.RoleName(cert.Subject.CommonName),
|
||||
slotID: slot,
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func listObjects(ctx IPKCS11Ctx, session pkcs11.SessionHandle) ([]pkcs11.ObjectHandle, error) {
|
||||
findTemplate := []*pkcs11.Attribute{
|
||||
pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_CERTIFICATE),
|
||||
}
|
||||
|
||||
if err := ctx.FindObjectsInit(session, findTemplate); err != nil {
|
||||
logrus.Debugf("Failed to init: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
objs, b, err := ctx.FindObjects(session, numSlots)
|
||||
for err == nil {
|
||||
var o []pkcs11.ObjectHandle
|
||||
o, b, err = ctx.FindObjects(session, numSlots)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if len(o) == 0 {
|
||||
break
|
||||
}
|
||||
objs = append(objs, o...)
|
||||
}
|
||||
if err != nil {
|
||||
logrus.Debugf("Failed to find: %s %v", err.Error(), b)
|
||||
if len(objs) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := ctx.FindObjectsFinal(session); err != nil {
|
||||
logrus.Debugf("Failed to finalize: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
return objs, nil
|
||||
}
|
||||
|
||||
func getNextEmptySlot(ctx IPKCS11Ctx, session pkcs11.SessionHandle) ([]byte, error) {
|
||||
findTemplate := []*pkcs11.Attribute{
|
||||
pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true),
|
||||
@@ -611,7 +621,7 @@ func getNextEmptySlot(ctx IPKCS11Ctx, session pkcs11.SessionHandle) ([]byte, err
|
||||
return []byte{byte(loc)}, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("Yubikey has no available slots.")
|
||||
return nil, errors.New("yubikey has no available slots")
|
||||
}
|
||||
|
||||
// YubiStore is a KeyStore for private keys inside a Yubikey
|
||||
@@ -687,7 +697,7 @@ func (s *YubiStore) AddKey(keyInfo trustmanager.KeyInfo, privKey data.PrivateKey
|
||||
|
||||
// Only add if we haven't seen the key already. Return whether the key was
|
||||
// added.
|
||||
func (s *YubiStore) addKey(keyID, role string, privKey data.PrivateKey) (
|
||||
func (s *YubiStore) addKey(keyID string, role data.RoleName, privKey data.PrivateKey) (
|
||||
bool, error) {
|
||||
|
||||
// We only allow adding root keys for now
|
||||
@@ -733,7 +743,7 @@ func (s *YubiStore) addKey(keyID, role string, privKey data.PrivateKey) (
|
||||
|
||||
// GetKey retrieves a key from the Yubikey only (it does not look inside the
|
||||
// backup store)
|
||||
func (s *YubiStore) GetKey(keyID string) (data.PrivateKey, string, error) {
|
||||
func (s *YubiStore) GetKey(keyID string) (data.PrivateKey, data.RoleName, error) {
|
||||
ctx, session, err := SetupHSMEnv(pkcs11Lib, s.libLoader)
|
||||
if err != nil {
|
||||
logrus.Debugf("No yubikey found, using alternative key storage: %s", err.Error())
|
||||
|
||||
+19
-6
@@ -6,12 +6,14 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/docker/notary/tuf/signed"
|
||||
"github.com/docker/notary/tuf/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const wildcard = "*"
|
||||
|
||||
// ErrValidationFail is returned when there is no valid trusted certificates
|
||||
// being served inside of the roots.json
|
||||
type ErrValidationFail struct {
|
||||
@@ -82,7 +84,7 @@ We shall call this: TOFUS.
|
||||
|
||||
Validation failure at any step will result in an ErrValidationFailed error.
|
||||
*/
|
||||
func ValidateRoot(prevRoot *data.SignedRoot, root *data.Signed, gun string, trustPinning TrustPinConfig) (*data.SignedRoot, error) {
|
||||
func ValidateRoot(prevRoot *data.SignedRoot, root *data.Signed, gun data.GUN, trustPinning TrustPinConfig) (*data.SignedRoot, error) {
|
||||
logrus.Debugf("entered ValidateRoot with dns: %s", gun)
|
||||
signedRoot, err := data.RootFromSigned(root)
|
||||
if err != nil {
|
||||
@@ -140,7 +142,7 @@ func ValidateRoot(prevRoot *data.SignedRoot, root *data.Signed, gun string, trus
|
||||
}
|
||||
|
||||
// Regardless of having a previous root or not, confirm that the new root validates against the trust pinning
|
||||
logrus.Debugf("checking root against trust_pinning config", gun)
|
||||
logrus.Debugf("checking root against trust_pinning config for %s", gun)
|
||||
trustPinCheckFunc, err := NewTrustPinChecker(trustPinning, gun, !havePrevRoot)
|
||||
if err != nil {
|
||||
return nil, &ErrValidationFail{Reason: err.Error()}
|
||||
@@ -175,16 +177,27 @@ func ValidateRoot(prevRoot *data.SignedRoot, root *data.Signed, gun string, trus
|
||||
return data.RootFromSigned(root)
|
||||
}
|
||||
|
||||
// MatchCNToGun checks that the common name in a cert is valid for the given gun.
|
||||
// This allows wildcards as suffixes, e.g. `namespace/*`
|
||||
func MatchCNToGun(commonName string, gun data.GUN) bool {
|
||||
if strings.HasSuffix(commonName, wildcard) {
|
||||
prefix := strings.TrimRight(commonName, wildcard)
|
||||
logrus.Debugf("checking gun %s against wildcard prefix %s", gun, prefix)
|
||||
return strings.HasPrefix(gun.String(), prefix)
|
||||
}
|
||||
return commonName == gun.String()
|
||||
}
|
||||
|
||||
// validRootLeafCerts returns a list of possibly (if checkExpiry is true) non-expired, non-sha1 certificates
|
||||
// found in root whose Common-Names match the provided GUN. Note that this
|
||||
// "validity" alone does not imply any measure of trust.
|
||||
func validRootLeafCerts(allLeafCerts map[string]*x509.Certificate, gun string, checkExpiry bool) (map[string]*x509.Certificate, error) {
|
||||
func validRootLeafCerts(allLeafCerts map[string]*x509.Certificate, gun data.GUN, checkExpiry bool) (map[string]*x509.Certificate, error) {
|
||||
validLeafCerts := make(map[string]*x509.Certificate)
|
||||
|
||||
// Go through every leaf certificate and check that the CN matches the gun
|
||||
for id, cert := range allLeafCerts {
|
||||
// Validate that this leaf certificate has a CN that matches the exact gun
|
||||
if cert.Subject.CommonName != gun {
|
||||
// Validate that this leaf certificate has a CN that matches the gun
|
||||
if !MatchCNToGun(cert.Subject.CommonName, gun) {
|
||||
logrus.Debugf("error leaf certificate CN: %s doesn't match the given GUN: %s",
|
||||
cert.Subject.CommonName, gun)
|
||||
continue
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user