docs/cmd/notary/main_test.go

526 lines
18 KiB
Go

package main
import (
"bytes"
"crypto/tls"
"fmt"
"io/ioutil"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/docker/go-connections/tlsconfig"
"github.com/docker/notary"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/server/storage"
"github.com/stretchr/testify/require"
)
// the default location for the config file is in ~/.notary/config.json - even if it doesn't exist.
func TestNotaryConfigFileDefault(t *testing.T) {
commander := &notaryCommander{
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
}
config, err := commander.parseConfig()
require.NoError(t, err)
configFileUsed := config.ConfigFileUsed()
require.True(t, strings.HasSuffix(configFileUsed,
filepath.Join(".notary", "config.json")), "Unknown config file: %s", configFileUsed)
}
// the default server address is notary-server
func TestRemoteServerDefault(t *testing.T) {
tempDir := tempDirWithConfig(t, "{}")
defer os.RemoveAll(tempDir)
configFile := filepath.Join(tempDir, "config.json")
commander := &notaryCommander{
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
}
// set a blank config file, so it doesn't check ~/.notary/config.json by default
// and execute a random command so that the flags are parsed
cmd := commander.GetCommand()
cmd.SetArgs([]string{"-c", configFile, "list"})
cmd.SetOutput(new(bytes.Buffer)) // eat the output
cmd.Execute()
config, err := commander.parseConfig()
require.NoError(t, err)
require.Equal(t, "https://notary-server:4443", getRemoteTrustServer(config))
}
// providing a config file uses the config file's server url instead
func TestRemoteServerUsesConfigFile(t *testing.T) {
tempDir := tempDirWithConfig(t, `{"remote_server": {"url": "https://myserver"}}`)
defer os.RemoveAll(tempDir)
configFile := filepath.Join(tempDir, "config.json")
commander := &notaryCommander{
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
}
// set a config file, so it doesn't check ~/.notary/config.json by default,
// and execute a random command so that the flags are parsed
cmd := commander.GetCommand()
cmd.SetArgs([]string{"-c", configFile, "list"})
cmd.SetOutput(new(bytes.Buffer)) // eat the output
cmd.Execute()
config, err := commander.parseConfig()
require.NoError(t, err)
require.Equal(t, "https://myserver", getRemoteTrustServer(config))
}
// a command line flag overrides the config file's server url
func TestRemoteServerCommandLineFlagOverridesConfig(t *testing.T) {
tempDir := tempDirWithConfig(t, `{"remote_server": {"url": "https://myserver"}}`)
defer os.RemoveAll(tempDir)
configFile := filepath.Join(tempDir, "config.json")
commander := &notaryCommander{
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
}
// set a config file, so it doesn't check ~/.notary/config.json by default,
// and execute a random command so that the flags are parsed
cmd := commander.GetCommand()
cmd.SetArgs([]string{"-c", configFile, "-s", "http://overridden", "list"})
cmd.SetOutput(new(bytes.Buffer)) // eat the output
cmd.Execute()
config, err := commander.parseConfig()
require.NoError(t, err)
require.Equal(t, "http://overridden", getRemoteTrustServer(config))
}
// invalid commands for `notary addhash`
func TestInvalidAddHashCommands(t *testing.T) {
tempDir := tempDirWithConfig(t, `{"remote_server": {"url": "https://myserver"}}`)
defer os.RemoveAll(tempDir)
configFile := filepath.Join(tempDir, "config.json")
b := new(bytes.Buffer)
cmd := NewNotaryCommand()
cmd.SetOutput(b)
// No hashes given
cmd.SetArgs(append([]string{"-c", configFile, "-d", tempDir}, "addhash", "gun", "test", "10"))
err := cmd.Execute()
require.Error(t, err)
require.Contains(t, err.Error(), "Must specify a GUN, target, byte size of target data, and at least one hash")
// Invalid byte size given
cmd = NewNotaryCommand()
cmd.SetArgs(append([]string{"-c", configFile, "-d", tempDir}, "addhash", "gun", "test", "sizeNotAnInt", "--sha256", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
err = cmd.Execute()
require.Error(t, err)
// Invalid sha256 size given
cmd = NewNotaryCommand()
cmd.SetArgs(append([]string{"-c", configFile, "-d", tempDir}, "addhash", "gun", "test", "1", "--sha256", "a"))
err = cmd.Execute()
require.Error(t, err)
require.Contains(t, err.Error(), "invalid sha256 hex contents provided")
// Invalid sha256 hex given
cmd = NewNotaryCommand()
cmd.SetArgs(append([]string{"-c", configFile, "-d", tempDir}, "addhash", "gun", "test", "1", "--sha256", "***aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa***"))
err = cmd.Execute()
require.Error(t, err)
// Invalid sha512 size given
cmd = NewNotaryCommand()
cmd.SetArgs(append([]string{"-c", configFile, "-d", tempDir}, "addhash", "gun", "test", "1", "--sha512", "a"))
err = cmd.Execute()
require.Error(t, err)
require.Contains(t, err.Error(), "invalid sha512 hex contents provided")
// Invalid sha512 hex given
cmd = NewNotaryCommand()
cmd.SetArgs(append([]string{"-c", configFile, "-d", tempDir}, "addhash", "gun", "test", "1", "--sha512", "***aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa******aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa***"))
err = cmd.Execute()
require.Error(t, err)
}
var exampleValidCommands = []string{
"init repo",
"list repo",
"status repo",
"publish repo",
"add repo v1 somefile",
"addhash repo targetv1 --sha256 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 10",
"verify repo v1",
"key list",
"key rotate repo snapshot",
"key generate rsa",
"key backup tempfile.zip",
"key export e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 backup.pem",
"key restore tempfile.zip",
"key import backup.pem",
"key remove e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"key passwd e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"delegation list repo",
"delegation add repo targets/releases path/to/pem/file.pem",
"delegation remove repo targets/releases",
}
// config parsing bugs are propagated in all commands
func TestConfigParsingErrorsPropagatedByCommands(t *testing.T) {
tempdir, err := ioutil.TempDir("", "empty-dir")
require.NoError(t, err)
defer os.RemoveAll(tempdir)
for _, args := range exampleValidCommands {
b := new(bytes.Buffer)
cmd := NewNotaryCommand()
cmd.SetOutput(b)
cmd.SetArgs(append(
[]string{"-c", filepath.Join(tempdir, "idonotexist.json"), "-d", tempdir},
strings.Fields(args)...))
err = cmd.Execute()
require.Error(t, err, "expected error when running `notary %s`", args)
require.Contains(t, err.Error(), "error opening config file", "running `notary %s`", args)
require.NotContains(t, b.String(), "Usage:")
}
}
// insufficient arguments produce an error before any parsing of configs happens
func TestInsufficientArgumentsReturnsErrorAndPrintsUsage(t *testing.T) {
tempdir, err := ioutil.TempDir("", "empty-dir")
require.NoError(t, err)
defer os.RemoveAll(tempdir)
for _, args := range exampleValidCommands {
b := new(bytes.Buffer)
cmd := NewNotaryCommand()
cmd.SetOutput(b)
arglist := strings.Fields(args)
if args == "key list" || args == "key generate rsa" {
// in these case, "key" or "key generate" are valid commands, so add an arg to them instead
arglist = append(arglist, "extraArg")
} else {
arglist = arglist[:len(arglist)-1]
}
invalid := strings.Join(arglist, " ")
cmd.SetArgs(append(
[]string{"-c", filepath.Join(tempdir, "idonotexist.json"), "-d", tempdir}, arglist...))
err = cmd.Execute()
require.NotContains(t, err.Error(), "error opening config file", "running `notary %s`", invalid)
// it's a usage error, so the usage is printed
require.Contains(t, b.String(), "Usage:", "expected usage when running `notary %s`", invalid)
}
}
// The bare notary command and bare subcommands all print out usage
func TestBareCommandPrintsUsageAndNoError(t *testing.T) {
tempdir, err := ioutil.TempDir("", "empty-dir")
require.NoError(t, err)
defer os.RemoveAll(tempdir)
// just the notary command
b := new(bytes.Buffer)
cmd := NewNotaryCommand()
cmd.SetOutput(b)
cmd.SetArgs([]string{"-c", filepath.Join(tempdir, "idonotexist.json")})
require.NoError(t, cmd.Execute(), "Expected no error from a help request")
// usage is printed
require.Contains(t, b.String(), "Usage:", "expected usage when running `notary`")
// notary key and notary delegation
for _, bareCommand := range []string{"key", "delegation"} {
b := new(bytes.Buffer)
cmd := NewNotaryCommand()
cmd.SetOutput(b)
cmd.SetArgs([]string{"-c", filepath.Join(tempdir, "idonotexist.json"), "-d", tempdir, bareCommand})
require.NoError(t, cmd.Execute(), "Expected no error from a help request")
// usage is printed
require.Contains(t, b.String(), "Usage:", "expected usage when running `notary %s`", bareCommand)
}
}
type recordingMetaStore struct {
gotten []string
storage.MemStorage
}
// GetCurrent gets the metadata from the underlying MetaStore, but also records
// that the metadata was requested
func (r *recordingMetaStore) GetCurrent(gun, role string) (*time.Time, []byte, error) {
r.gotten = append(r.gotten, fmt.Sprintf("%s.%s", gun, role))
return r.MemStorage.GetCurrent(gun, role)
}
// GetChecksum gets the metadata from the underlying MetaStore, but also records
// that the metadata was requested
func (r *recordingMetaStore) GetChecksum(gun, role, checksum string) (*time.Time, []byte, error) {
r.gotten = append(r.gotten, fmt.Sprintf("%s.%s", gun, role))
return r.MemStorage.GetChecksum(gun, role, checksum)
}
// the config can provide all the TLS information necessary - the root ca file,
// the tls client files - they are all relative to the directory of the config
// file, and not the cwd
func TestConfigFileTLSCannotBeRelativeToCWD(t *testing.T) {
// Set up server that with a self signed cert
var err error
// add a handler for getting the root
m := &recordingMetaStore{MemStorage: *storage.NewMemStorage()}
s := httptest.NewUnstartedServer(setupServerHandler(m))
s.TLS, err = tlsconfig.Server(tlsconfig.Options{
CertFile: "../../fixtures/notary-server.crt",
KeyFile: "../../fixtures/notary-server.key",
CAFile: "../../fixtures/root-ca.crt",
ClientAuth: tls.RequireAndVerifyClientCert,
})
require.NoError(t, err)
s.StartTLS()
defer s.Close()
// test that a config file with certs that are relative to the cwd fail
tempDir := tempDirWithConfig(t, fmt.Sprintf(`{
"remote_server": {
"url": "%s",
"root_ca": "../../fixtures/root-ca.crt",
"tls_client_cert": "../../fixtures/notary-server.crt",
"tls_client_key": "../../fixtures/notary-server.key"
}
}`, s.URL))
defer os.RemoveAll(tempDir)
configFile := filepath.Join(tempDir, "config.json")
// set a config file, so it doesn't check ~/.notary/config.json by default,
// and execute a random command so that the flags are parsed
cmd := NewNotaryCommand()
cmd.SetArgs([]string{"-c", configFile, "-d", tempDir, "list", "repo"})
cmd.SetOutput(new(bytes.Buffer)) // eat the output
err = cmd.Execute()
require.Error(t, err, "expected a failure due to TLS")
require.Contains(t, err.Error(), "TLS", "should have been a TLS error")
// validate that we failed to connect and attempt any downloads at all
require.Len(t, m.gotten, 0)
}
// the config can provide all the TLS information necessary - the root ca file,
// the tls client files - they are all relative to the directory of the config
// file, and not the cwd, or absolute paths
func TestConfigFileTLSCanBeRelativeToConfigOrAbsolute(t *testing.T) {
// Set up server that with a self signed cert
var err error
// add a handler for getting the root
m := &recordingMetaStore{MemStorage: *storage.NewMemStorage()}
s := httptest.NewUnstartedServer(setupServerHandler(m))
s.TLS, err = tlsconfig.Server(tlsconfig.Options{
CertFile: "../../fixtures/notary-server.crt",
KeyFile: "../../fixtures/notary-server.key",
CAFile: "../../fixtures/root-ca.crt",
ClientAuth: tls.RequireAndVerifyClientCert,
})
require.NoError(t, err)
s.StartTLS()
defer s.Close()
tempDir, err := ioutil.TempDir("", "config-test")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
configFile, err := os.Create(filepath.Join(tempDir, "config.json"))
require.NoError(t, err)
fmt.Fprintf(configFile, `{
"remote_server": {
"url": "%s",
"root_ca": "root-ca.crt",
"tls_client_cert": "%s",
"tls_client_key": "notary-server.key"
}
}`, s.URL, filepath.Join(tempDir, "notary-server.crt"))
configFile.Close()
// copy the certs to be relative to the config directory
for _, fname := range []string{"notary-server.crt", "notary-server.key", "root-ca.crt"} {
content, err := ioutil.ReadFile(filepath.Join("../../fixtures", fname))
require.NoError(t, err)
require.NoError(t, ioutil.WriteFile(filepath.Join(tempDir, fname), content, 0766))
}
// set a config file, so it doesn't check ~/.notary/config.json by default,
// and execute a random command so that the flags are parsed
cmd := NewNotaryCommand()
cmd.SetArgs([]string{"-c", configFile.Name(), "-d", tempDir, "list", "repo"})
cmd.SetOutput(new(bytes.Buffer)) // eat the output
err = cmd.Execute()
require.Error(t, err, "there was no repository, so list should have failed")
require.NotContains(t, err.Error(), "TLS", "there was no TLS error though!")
// validate that we actually managed to connect and attempted to download the root though
require.Len(t, m.gotten, 1)
require.Equal(t, m.gotten[0], "repo.root")
}
// Whatever TLS config is in the config file can be overridden by the command line
// TLS flags, which are relative to the CWD (not the config) or absolute
func TestConfigFileOverridenByCmdLineFlags(t *testing.T) {
// Set up server that with a self signed cert
var err error
// add a handler for getting the root
m := &recordingMetaStore{MemStorage: *storage.NewMemStorage()}
s := httptest.NewUnstartedServer(setupServerHandler(m))
s.TLS, err = tlsconfig.Server(tlsconfig.Options{
CertFile: "../../fixtures/notary-server.crt",
KeyFile: "../../fixtures/notary-server.key",
CAFile: "../../fixtures/root-ca.crt",
ClientAuth: tls.RequireAndVerifyClientCert,
})
require.NoError(t, err)
s.StartTLS()
defer s.Close()
tempDir := tempDirWithConfig(t, fmt.Sprintf(`{
"remote_server": {
"url": "%s",
"root_ca": "nope",
"tls_client_cert": "nope",
"tls_client_key": "nope"
}
}`, s.URL))
defer os.RemoveAll(tempDir)
configFile := filepath.Join(tempDir, "config.json")
// set a config file, so it doesn't check ~/.notary/config.json by default,
// and execute a random command so that the flags are parsed
cwd, err := os.Getwd()
require.NoError(t, err)
cmd := NewNotaryCommand()
cmd.SetArgs([]string{
"-c", configFile, "-d", tempDir, "list", "repo",
"--tlscacert", "../../fixtures/root-ca.crt",
"--tlscert", filepath.Clean(filepath.Join(cwd, "../../fixtures/notary-server.crt")),
"--tlskey", "../../fixtures/notary-server.key"})
cmd.SetOutput(new(bytes.Buffer)) // eat the output
err = cmd.Execute()
require.Error(t, err, "there was no repository, so list should have failed")
require.NotContains(t, err.Error(), "TLS", "there was no TLS error though!")
// validate that we actually managed to connect and attempted to download the root though
require.Len(t, m.gotten, 1)
require.Equal(t, m.gotten[0], "repo.root")
}
// the config can specify trust pinning settings for TOFUs, as well as pinned Certs or CA
func TestConfigFileTrustPinning(t *testing.T) {
var err error
tempDir := tempDirWithConfig(t, `{
"trust_pinning": {
"disable_tofu": false
}
}`)
defer os.RemoveAll(tempDir)
commander := &notaryCommander{
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
configFile: filepath.Join(tempDir, "config.json"),
}
// Check that tofu was set correctly
config, err := commander.parseConfig()
require.NoError(t, err)
require.Equal(t, false, config.GetBool("trust_pinning.disable_tofu"))
trustPin, err := getTrustPinning(config)
require.NoError(t, err)
require.Equal(t, false, trustPin.DisableTOFU)
tempDir = tempDirWithConfig(t, `{
"remote_server": {
"url": "%s"
},
"trust_pinning": {
"disable_tofu": true
}
}`)
defer os.RemoveAll(tempDir)
commander = &notaryCommander{
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
configFile: filepath.Join(tempDir, "config.json"),
}
// Check that tofu was correctly disabled
config, err = commander.parseConfig()
require.NoError(t, err)
require.Equal(t, true, config.GetBool("trust_pinning.disable_tofu"))
trustPin, err = getTrustPinning(config)
require.NoError(t, err)
require.Equal(t, true, trustPin.DisableTOFU)
tempDir = tempDirWithConfig(t, fmt.Sprintf(`{
"trust_pinning": {
"certs": {
"repo3": ["%s"]
}
}
}`, strings.Repeat("x", notary.Sha256HexSize)))
defer os.RemoveAll(tempDir)
commander = &notaryCommander{
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
configFile: filepath.Join(tempDir, "config.json"),
}
config, err = commander.parseConfig()
require.NoError(t, err)
require.Equal(t, []interface{}{strings.Repeat("x", notary.Sha256HexSize)}, config.GetStringMap("trust_pinning.certs")["repo3"])
trustPin, err = getTrustPinning(config)
require.NoError(t, err)
require.Equal(t, strings.Repeat("x", notary.Sha256HexSize), trustPin.Certs["repo3"][0])
// Check that an invalid cert ID pinning format fails
tempDir = tempDirWithConfig(t, fmt.Sprintf(`{
"trust_pinning": {
"certs": {
"repo3": "%s"
}
}
}`, strings.Repeat("x", notary.Sha256HexSize)))
defer os.RemoveAll(tempDir)
commander = &notaryCommander{
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
configFile: filepath.Join(tempDir, "config.json"),
}
config, err = commander.parseConfig()
require.NoError(t, err)
trustPin, err = getTrustPinning(config)
require.Error(t, err)
tempDir = tempDirWithConfig(t, fmt.Sprintf(`{
"trust_pinning": {
"ca": {
"repo4": "%s"
}
}
}`, "root-ca.crt"))
defer os.RemoveAll(tempDir)
commander = &notaryCommander{
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
configFile: filepath.Join(tempDir, "config.json"),
}
config, err = commander.parseConfig()
require.NoError(t, err)
require.Equal(t, "root-ca.crt", config.GetStringMap("trust_pinning.ca")["repo4"])
trustPin, err = getTrustPinning(config)
require.NoError(t, err)
require.Equal(t, "root-ca.crt", trustPin.CA["repo4"])
}