fleet/ee/fleetctl/updates_test.go
Lucas Manuel Rodriguez 832b29f8c7
Update go-tuf to v0.5.0 (bis) (#8112)
* Update go-tuf to v0.5.0

This was triggered by the security advisory
[GHSA-3633-5h82-39pq](https://github.com/theupdateframework/go-tuf/security/advisories/GHSA-3633-5h82-39pq).
Fleet's use of go-tuf is not vulnerable to this issue due to not using
key thresholds greater than 1.

There were some API changes that necessitate changing the initialization
code for the TUF client. See
https://github.com/theupdateframework/go-tuf/issues/379 for further
discussion.

* Add changes file

* Update default root metadata

* Add review changes to update-go-tuf branch

* Update tests

* Add more checks to roots output

Co-authored-by: Zach Wasserman <zach@fleetdm.com>
2022-10-07 17:03:39 -03:00

460 lines
15 KiB
Go

//go:build darwin || linux
// +build darwin linux
package eefleetctl
import (
"archive/tar"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
"github.com/fleetdm/fleet/v4/orbit/pkg/update"
"github.com/fleetdm/fleet/v4/orbit/pkg/update/filestore"
"github.com/fleetdm/fleet/v4/pkg/secure"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
)
func TestPassphraseHandlerEnvironment(t *testing.T) {
// Not t.Parallel() due to modifications to environment.
testCases := []struct {
role string
passphrase string
}{
{role: "root", passphrase: "rootpassphrase"},
{role: "timestamp", passphrase: "timestamp5#$#@"},
{role: "snapshot", passphrase: "snapshot$#@"},
{role: "targets", passphrase: "$#^#$@targets"},
}
for _, tt := range testCases {
t.Run(tt.role, func(t *testing.T) {
tt := tt
handler := newPassphraseHandler()
envKey := fmt.Sprintf("FLEET_%s_PASSPHRASE", strings.ToUpper(tt.role))
t.Setenv(envKey, tt.passphrase)
passphrase, err := handler.getPassphrase(tt.role, false, false)
require.NoError(t, err)
assert.Equal(t, tt.passphrase, string(passphrase))
// Should work second time with cache
passphrase, err = handler.getPassphrase(tt.role, false, false)
require.NoError(t, err)
assert.Equal(t, tt.passphrase, string(passphrase))
})
}
}
func TestPassphraseHandlerEmpty(t *testing.T) {
// Not t.Parallel() due to modifications to environment.
handler := newPassphraseHandler()
t.Setenv("FLEET_ROOT_PASSPHRASE", "")
_, err := handler.getPassphrase("root", false, false)
require.Error(t, err)
}
func setPassphrases(t *testing.T) {
t.Helper()
t.Setenv("FLEET_ROOT_PASSPHRASE", "root")
t.Setenv("FLEET_TIMESTAMP_PASSPHRASE", "timestamp")
t.Setenv("FLEET_TARGETS_PASSPHRASE", "targets")
t.Setenv("FLEET_SNAPSHOT_PASSPHRASE", "snapshot")
}
func runUpdatesCommand(args ...string) error {
app := cli.NewApp()
app.Commands = []*cli.Command{UpdatesCommand()}
return app.Run(append([]string{os.Args[0], "updates"}, args...))
}
func TestUpdatesInit(t *testing.T) {
// Not t.Parallel() due to modifications to environment.
tmpDir := t.TempDir()
setPassphrases(t)
require.NoError(t, runUpdatesCommand("init", "--path", tmpDir))
// Should fail with already initialized
require.Error(t, runUpdatesCommand("init", "--path", tmpDir))
}
func TestUpdatesErrorInvalidPassphrase(t *testing.T) {
// Not t.Parallel() due to modifications to environment.
tmpDir := t.TempDir()
setPassphrases(t)
require.NoError(t, runUpdatesCommand("init", "--path", tmpDir))
// Should not be able to add with invalid passphrase
t.Setenv("FLEET_SNAPSHOT_PASSPHRASE", "invalid")
// Reset the cache that already has correct passwords stored
passHandler = newPassphraseHandler()
require.Error(t, runUpdatesCommand("add", "--path", tmpDir, "--target", "anything", "--platform", "windows", "--name", "test", "--version", "1.3.4.7"))
}
func TestUpdatesInitKeysInitializedError(t *testing.T) {
// Not t.Parallel() due to modifications to environment.
tmpDir := t.TempDir()
setPassphrases(t)
// Create an empty "keys" directory
require.NoError(t, os.Mkdir(filepath.Join(tmpDir, "keys"), os.ModePerm|os.ModeDir))
// Should fail with already initialized
require.Error(t, runUpdatesCommand("init", "--path", tmpDir))
}
func assertFileExists(t *testing.T, path string) {
t.Helper()
st, err := os.Stat(path)
require.NoError(t, err, "stat should succeed")
assert.True(t, st.Mode().IsRegular(), "should be regular file: %s", path)
}
func assertVersion(t *testing.T, expected int64, versionFunc func() (int64, error)) {
t.Helper()
actual, err := versionFunc()
require.NoError(t, err)
assert.Equal(t, expected, actual)
}
// Capture stdout while running the updates roots command
func getRoots(t *testing.T, tmpDir string) string {
t.Helper()
stdout := os.Stdout
defer func() { os.Stdout = stdout }()
r, w, err := os.Pipe()
require.NoError(t, err)
os.Stdout = w
require.NoError(t, runUpdatesCommand("roots", "--path", tmpDir))
require.NoError(t, w.Close())
out, err := ioutil.ReadAll(r)
require.NoError(t, err)
// Check output contains the root.json
var keys map[string]interface{}
require.NoError(t, json.Unmarshal(out, &keys))
signed_ := keys["signed"]
require.NotNil(t, signed_)
signed, ok := signed_.(map[string]interface{})
require.True(t, ok)
keys_ := signed["keys"]
require.NotNil(t, keys_)
require.NotEmpty(t, signed["keys"])
keys, ok = keys_.(map[string]interface{})
require.True(t, ok)
require.NotEmpty(t, keys)
// Get first key (map key is the identifier of the key).
var key map[string]interface{}
for _, key_ := range keys {
key, ok = key_.(map[string]interface{})
require.True(t, ok)
break
}
require.NotEmpty(t, key)
require.Equal(t, "ed25519", key["scheme"])
require.Equal(t, "ed25519", key["keytype"])
keyval_, ok := key["keyval"].(map[string]interface{})
require.True(t, ok)
require.NotEmpty(t, keyval_["public"]) // keyval_["public"] contains the public key.
return string(out)
}
func compressSingleFile(t *testing.T, filePath, outFilePath string) {
outf, err := secure.OpenFile(outFilePath, os.O_CREATE|os.O_WRONLY, constant.DefaultFileMode)
require.NoError(t, err)
defer outf.Close()
gw := gzip.NewWriter(outf)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()
inf, err := os.Open(filePath)
require.NoError(t, err)
infi, err := inf.Stat()
require.NoError(t, err)
err = tw.WriteHeader(
&tar.Header{
Name: filepath.Base(filePath),
Size: infi.Size(),
Mode: 0o777,
},
)
require.NoError(t, err)
_, err = io.Copy(tw, inf)
require.NoError(t, err)
err = tw.Close()
require.NoError(t, err)
err = gw.Close()
require.NoError(t, err)
err = outf.Close()
require.NoError(t, err)
}
func TestUpdatesIntegration(t *testing.T) {
// Not t.Parallel() due to modifications to environment.
tmpDir := t.TempDir()
setPassphrases(t)
require.NoError(t, runUpdatesCommand("init", "--path", tmpDir))
// Run an HTTP server to serve the update metadata
server := httptest.NewServer(http.FileServer(http.Dir(filepath.Join(tmpDir, "repository"))))
t.Cleanup(server.Close)
roots := getRoots(t, tmpDir)
// Use the current binary as target for this test so that it is a binary that
// is valid for execution on the current system.
testPath, err := os.Executable()
require.NoError(t, err)
tarGzFilePath := filepath.Join(filepath.Dir(testPath), "other.app.tar.gz")
compressSingleFile(t, testPath, tarGzFilePath)
// Initialize an update client
localStore, err := filestore.New(filepath.Join(tmpDir, "tuf-metadata.json"))
require.NoError(t, err)
updater, err := update.NewUpdater(update.Options{
RootDirectory: tmpDir,
ServerURL: server.URL,
RootKeys: roots,
LocalStore: localStore,
Targets: update.Targets{
"test": update.TargetInfo{
Platform: "macos",
Channel: "1.3.3.7",
TargetFile: "test",
},
"other": update.TargetInfo{
Platform: "macos-app",
Channel: "1.3.3.8",
TargetFile: "other.app.tar.gz",
ExtractedExecSubPath: []string{filepath.Base(testPath)},
},
},
})
require.NoError(t, err)
require.NoError(t, updater.UpdateMetadata())
_, err = updater.Lookup("any")
require.Error(t, err, "lookup should fail before targets added")
repo, err := openRepo(tmpDir)
require.NoError(t, err)
assertVersion(t, 1, repo.RootVersion)
assertVersion(t, 1, repo.TargetsVersion)
assertVersion(t, 1, repo.SnapshotVersion)
assertVersion(t, 1, repo.TimestampVersion)
// Add some targets
require.NoError(t, runUpdatesCommand("add", "--path", tmpDir, "--target", testPath, "--platform", "linux", "--name", "test", "--version", "1.3.3.7"))
require.NoError(t, runUpdatesCommand("add", "--path", tmpDir, "--target", testPath, "--platform", "macos", "--name", "test", "--version", "1.3.3.7"))
require.NoError(t, runUpdatesCommand("add", "--path", tmpDir, "--target", testPath, "--platform", "windows", "--name", "test", "--version", "1.3.3.7"))
require.NoError(t, runUpdatesCommand("add", "--path", tmpDir, "--target", tarGzFilePath, "--platform", "macos-app", "--name", "other", "--version", "1.3.3.8"))
assertFileExists(t, filepath.Join(tmpDir, "repository", "targets", "test", "linux", "1.3.3.7", "test"))
assertFileExists(t, filepath.Join(tmpDir, "repository", "targets", "test", "macos", "1.3.3.7", "test"))
assertFileExists(t, filepath.Join(tmpDir, "repository", "targets", "test", "windows", "1.3.3.7", "test"))
assertFileExists(t, filepath.Join(tmpDir, "repository", "targets", "other", "macos-app", "1.3.3.8", "other.app.tar.gz"))
// Verify the client can look up and download the updates
require.NoError(t, updater.UpdateMetadata())
targets, err := updater.Targets()
require.NoError(t, err)
assert.Len(t, targets, 4)
_, err = updater.Get("test")
require.NoError(t, err)
other, err := updater.Get("other")
require.NoError(t, err)
require.Equal(t, filepath.Base(other.ExecPath), filepath.Base(testPath))
repo, err = openRepo(tmpDir)
require.NoError(t, err)
assertVersion(t, 1, repo.RootVersion)
assertVersion(t, 5, repo.TargetsVersion)
assertVersion(t, 5, repo.SnapshotVersion)
assertVersion(t, 5, repo.TimestampVersion)
require.NoError(t, runUpdatesCommand("timestamp", "--path", tmpDir))
repo, err = openRepo(tmpDir)
require.NoError(t, err)
assertVersion(t, 1, repo.RootVersion)
assertVersion(t, 5, repo.TargetsVersion)
assertVersion(t, 5, repo.SnapshotVersion)
assertVersion(t, 6, repo.TimestampVersion)
// Rotate root
require.NoError(t, runUpdatesCommand("rotate", "--path", tmpDir, "root"))
repo, err = openRepo(tmpDir)
require.NoError(t, err)
assertVersion(t, 2, repo.RootVersion)
assertVersion(t, 6, repo.TargetsVersion)
assertVersion(t, 6, repo.SnapshotVersion)
assertVersion(t, 7, repo.TimestampVersion)
// Rotate targets
require.NoError(t, runUpdatesCommand("rotate", "--path", tmpDir, "targets"))
repo, err = openRepo(tmpDir)
require.NoError(t, err)
assertVersion(t, 3, repo.RootVersion)
assertVersion(t, 7, repo.TargetsVersion)
assertVersion(t, 7, repo.SnapshotVersion)
assertVersion(t, 8, repo.TimestampVersion)
// Rotate snapshot
require.NoError(t, runUpdatesCommand("rotate", "--path", tmpDir, "snapshot"))
repo, err = openRepo(tmpDir)
require.NoError(t, err)
assertVersion(t, 4, repo.RootVersion)
assertVersion(t, 8, repo.TargetsVersion)
assertVersion(t, 8, repo.SnapshotVersion)
assertVersion(t, 9, repo.TimestampVersion)
// Rotate timestamp
require.NoError(t, runUpdatesCommand("rotate", "--path", tmpDir, "timestamp"))
repo, err = openRepo(tmpDir)
require.NoError(t, err)
assertVersion(t, 5, repo.RootVersion)
assertVersion(t, 9, repo.TargetsVersion)
assertVersion(t, 9, repo.SnapshotVersion)
assertVersion(t, 10, repo.TimestampVersion)
// Should still be able to add after rotations
require.NoError(t, runUpdatesCommand("add", "--path", tmpDir, "--target", testPath, "--platform", "windows", "--name", "test", "--version", "1.3.3.7"))
// Root key should have changed
newRoots := getRoots(t, tmpDir)
assert.NotEqual(t, roots, newRoots)
// Should still be able to retrieve an update after rotations
require.NoError(t, updater.UpdateMetadata())
targets, err = updater.Targets()
require.NoError(t, err)
assert.Len(t, targets, 4)
// Remove the old test copy first
p, err := updater.ExecutableLocalPath("test")
require.NoError(t, err)
require.NoError(t, os.RemoveAll(p))
_, err = updater.Get("test")
require.NoError(t, err)
// Remove the old other copy first
o, err := updater.Get("other")
require.NoError(t, err)
require.NoError(t, os.RemoveAll(filepath.Join(filepath.Dir(o.ExecPath), "other.app.tar.gz")))
require.NoError(t, os.RemoveAll(filepath.Join(filepath.Dir(o.ExecPath), filepath.Base(testPath))))
o2, err := updater.Get("other")
require.NoError(t, err)
require.Equal(t, o, o2)
_, err = os.Stat(o2.ExecPath)
require.NoError(t, err)
// Update client should be able to initialize with new root
tmpDir = t.TempDir()
localStore, err = filestore.New(filepath.Join(tmpDir, "tuf-metadata.json"))
require.NoError(t, err)
updater, err = update.NewUpdater(update.Options{RootDirectory: tmpDir, ServerURL: server.URL, RootKeys: roots, LocalStore: localStore})
require.NoError(t, err)
require.NoError(t, updater.UpdateMetadata())
}
func TestCommit(t *testing.T) {
tmpDir := t.TempDir()
setPassphrases(t)
require.NoError(t, runUpdatesCommand("init", "--path", tmpDir))
initialEntries, err := os.ReadDir(filepath.Join(tmpDir, "repository"))
require.NoError(t, err)
initialRoots := getRoots(t, tmpDir)
commit, _, err := startRotatePseudoTx(tmpDir)
require.NoError(t, err)
repo, err := openRepo(tmpDir)
require.NoError(t, err)
// Make rotations that change repo
require.NoError(t, updatesGenKey(repo, "root"))
require.NoError(t, repo.Sign("root.json"))
require.NoError(t, repo.SnapshotWithExpires(time.Now().Add(snapshotExpirationDuration)))
require.NoError(t, repo.TimestampWithExpires(time.Now().Add(timestampExpirationDuration)))
require.NoError(t, repo.Commit())
// Assert directory has changed after commit.
require.NoError(t, commit())
entries, err := os.ReadDir(filepath.Join(tmpDir, "repository"))
require.NoError(t, err)
assert.NotEqual(t, initialEntries, entries)
_, err = os.Stat(filepath.Join(tmpDir, "repository", ".backup"))
assert.True(t, os.IsNotExist(err))
// Roots should have changed.
roots := getRoots(t, tmpDir)
assert.NotEqual(t, initialRoots, roots)
}
func TestRollback(t *testing.T) {
tmpDir := t.TempDir()
setPassphrases(t)
require.NoError(t, runUpdatesCommand("init", "--path", tmpDir))
initialEntries, err := os.ReadDir(filepath.Join(tmpDir, "repository"))
require.NoError(t, err)
initialRoots := getRoots(t, tmpDir)
_, rollback, err := startRotatePseudoTx(tmpDir)
require.NoError(t, err)
repo, err := openRepo(tmpDir)
require.NoError(t, err)
// Make rotations that change repo
require.NoError(t, updatesGenKey(repo, "root"))
require.NoError(t, repo.Sign("root.json"))
require.NoError(t, repo.SnapshotWithExpires(time.Now().Add(snapshotExpirationDuration)))
require.NoError(t, repo.TimestampWithExpires(time.Now().Add(timestampExpirationDuration)))
require.NoError(t, repo.Commit())
// Assert directory has NOT changed after rollback.
require.NoError(t, rollback())
entries, err := os.ReadDir(filepath.Join(tmpDir, "repository"))
require.NoError(t, err)
assert.Equal(t, initialEntries, entries)
_, err = os.Stat(filepath.Join(tmpDir, "repository", ".backup"))
assert.True(t, os.IsNotExist(err))
// Roots should NOT have changed.
repo, err = openRepo(tmpDir)
require.NoError(t, err)
roots := getRoots(t, tmpDir)
assert.Equal(t, initialRoots, roots)
}