mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
832b29f8c7
* 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>
460 lines
15 KiB
Go
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)
|
|
}
|