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 := ¬aryCommander{ 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 := ¬aryCommander{ 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 := ¬aryCommander{ 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 := ¬aryCommander{ 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 := ¬aryCommander{ 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 = ¬aryCommander{ 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 = ¬aryCommander{ 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 = ¬aryCommander{ 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 = ¬aryCommander{ 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"]) }