mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
7531ac20db
This resolves an issue with adding updates on a macOS 13 machine. It seems like macOS may have changed the default directory permissions and these new stricter permissions are compatible with that default. This is the error that was encountered before these changes: ``` Error: create dst dir for copy: Path staged/targets already exists with mode 20000000700 instead of the expected 20000000755 ``` # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [x] Manual QA for all new/changed functionality
768 lines
21 KiB
Go
768 lines
21 KiB
Go
//go:build darwin || linux
|
|
// +build darwin linux
|
|
|
|
package eefleetctl
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
|
|
"github.com/fleetdm/fleet/v4/pkg/file"
|
|
"github.com/fleetdm/fleet/v4/pkg/secure"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/theupdateframework/go-tuf"
|
|
"github.com/urfave/cli/v2"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
)
|
|
|
|
const (
|
|
// consistentSnapshots are not needed due to the low update frequency of
|
|
// these repositories.
|
|
consistentSnapshots = false
|
|
|
|
// ~10 years
|
|
keyExpirationDuration = 10 * 365 * 24 * time.Hour
|
|
|
|
// Expirations from
|
|
// https://github.com/theupdateframework/notary/blob/e87b31f46cdc5041403c64b7536df236d5e35860/docs/best_practices.md#expiration-prevention
|
|
// ~10 years
|
|
rootExpirationDuration = 10 * 365 * 24 * time.Hour //nolint:unused,deadcode
|
|
// ~3 years
|
|
targetsExpirationDuration = 3 * 365 * 24 * time.Hour
|
|
// ~3 years
|
|
snapshotExpirationDuration = 3 * 365 * 24 * time.Hour
|
|
// 14 days
|
|
timestampExpirationDuration = 14 * 24 * time.Hour
|
|
|
|
decryptionFailedError = "encrypted: decryption failed"
|
|
|
|
backupDirectory = ".backup"
|
|
)
|
|
|
|
var passHandler = newPassphraseHandler()
|
|
|
|
func UpdatesCommand() *cli.Command {
|
|
return &cli.Command{
|
|
Name: "updates",
|
|
Usage: "Manage client updates",
|
|
Description: `fleetctl updates commands provide the initialization and management of a TUF-compliant update repository.
|
|
|
|
This functionality is licensed under the Fleet EE License. Usage requires a current Fleet EE subscription.`,
|
|
Subcommands: []*cli.Command{
|
|
updatesInitCommand(),
|
|
updatesRootsCommand(),
|
|
updatesAddCommand(),
|
|
updatesTimestampCommand(),
|
|
updatesRotateCommand(),
|
|
},
|
|
}
|
|
}
|
|
|
|
func updatesFlags() []cli.Flag {
|
|
return []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: "path",
|
|
Usage: "Path to local repository",
|
|
Value: ".",
|
|
},
|
|
}
|
|
}
|
|
|
|
func updatesInitCommand() *cli.Command {
|
|
return &cli.Command{
|
|
Name: "init",
|
|
Usage: "Initialize update repository",
|
|
Flags: updatesFlags(),
|
|
Action: updatesInitFunc,
|
|
}
|
|
}
|
|
|
|
func updatesInitFunc(c *cli.Context) error {
|
|
path := c.String("path")
|
|
store := tuf.FileSystemStore(path, passHandler.getPassphrase)
|
|
meta, err := store.GetMeta()
|
|
if err != nil {
|
|
return fmt.Errorf("get repo meta: %w", err)
|
|
}
|
|
if len(meta) != 0 {
|
|
return fmt.Errorf("repo already initialized: %s", path)
|
|
}
|
|
// Ensure no existing keys before initializing
|
|
if _, err := os.Stat(filepath.Join(path, "keys")); !errors.Is(err, os.ErrNotExist) {
|
|
if err == nil {
|
|
return fmt.Errorf("keys directory already exists: %s", filepath.Join(path, "keys"))
|
|
}
|
|
return fmt.Errorf("failed to check existence of keys directory: %w", err)
|
|
}
|
|
|
|
repo, err := tuf.NewRepo(store)
|
|
if err != nil {
|
|
return fmt.Errorf("open repo: %w", err)
|
|
}
|
|
|
|
// TODO messaging about using a secure environment
|
|
|
|
// Explicitly initialize with consistent snapshots turned off.
|
|
if err := repo.Init(consistentSnapshots); err != nil {
|
|
return fmt.Errorf("initialize repo: %w", err)
|
|
}
|
|
|
|
// Generate keys
|
|
for _, role := range []string{"root", "targets", "snapshot", "timestamp"} {
|
|
// TODO don't fatal here if passwords don't match
|
|
if err := updatesGenKey(repo, role); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Sign roots metadata
|
|
if err := repo.Sign("root.json"); err != nil {
|
|
return fmt.Errorf("sign root metadata: %w", err)
|
|
}
|
|
|
|
// Create empty manifests for commit
|
|
if err := repo.AddTargetsWithExpires(
|
|
nil,
|
|
nil,
|
|
time.Now().Add(targetsExpirationDuration),
|
|
); err != nil {
|
|
return fmt.Errorf("initialize targets: %w", err)
|
|
}
|
|
if err := repo.SnapshotWithExpires(time.Now().Add(snapshotExpirationDuration)); err != nil {
|
|
return fmt.Errorf("make snapshot: %w", err)
|
|
}
|
|
if err := repo.TimestampWithExpires(time.Now().Add(timestampExpirationDuration)); err != nil {
|
|
return fmt.Errorf("make timestamp: %w", err)
|
|
}
|
|
|
|
// Commit empty manifests
|
|
if err := repo.Commit(); err != nil {
|
|
return fmt.Errorf("commit repo: %w", err)
|
|
}
|
|
|
|
// TODO messaging about separating keys -- maybe we can help by splitting
|
|
// things up into separate directories?
|
|
|
|
return nil
|
|
}
|
|
|
|
func updatesRootsCommand() *cli.Command {
|
|
return &cli.Command{
|
|
Name: "roots",
|
|
Usage: "Get root metadata",
|
|
Flags: updatesFlags(),
|
|
Action: updatesRootsFunc,
|
|
}
|
|
}
|
|
|
|
func updatesRootsFunc(c *cli.Context) error {
|
|
repo, err := openRepo(c.String("path"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
meta, err := repo.GetMeta()
|
|
if err != nil {
|
|
return fmt.Errorf("get repo metadata: %w", err)
|
|
}
|
|
rootMeta := meta["root.json"]
|
|
if rootMeta == nil {
|
|
return errors.New("missing root metadata")
|
|
}
|
|
|
|
fmt.Println(string(rootMeta))
|
|
|
|
return nil
|
|
}
|
|
|
|
func updatesAddCommand() *cli.Command {
|
|
return &cli.Command{
|
|
Name: "add",
|
|
Usage: "Add a new update artifact",
|
|
Flags: append(updatesFlags(),
|
|
&cli.StringFlag{
|
|
Name: "target",
|
|
Required: true,
|
|
Usage: "Path to target (required)",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "name",
|
|
Required: true,
|
|
Usage: "Name of target (required)",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "platform",
|
|
Required: true,
|
|
Usage: "Platform name of target (required)",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "version",
|
|
Required: true,
|
|
Usage: "Version of target (required)",
|
|
},
|
|
&cli.StringSliceFlag{
|
|
Name: "tag",
|
|
Aliases: []string{"t"},
|
|
Usage: "Tags to apply to the target (multiple may be specified)",
|
|
},
|
|
),
|
|
Action: updatesAddFunc,
|
|
}
|
|
}
|
|
|
|
func updatesAddFunc(c *cli.Context) error {
|
|
repo, err := openRepo(c.String("path"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := checkKeys(c.String("path"),
|
|
"timestamp",
|
|
"snapshot",
|
|
"targets",
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
tags := c.StringSlice("tag")
|
|
version := c.String("version")
|
|
platform := c.String("platform")
|
|
name := c.String("name")
|
|
target := c.String("target")
|
|
|
|
targetsPath := filepath.Join(c.String("path"), "staged", "targets")
|
|
|
|
var paths []string
|
|
for _, tag := range append([]string{version}, tags...) {
|
|
// NOTE(lucas): "updates add" expects the target file to match the target name.
|
|
// E.g.
|
|
// - an ".app.tar.gz" file for target=osqueryd is expected to be called "osqueryd.app.tar.gz".
|
|
// - an ".exe" file for target=osqueryd is expected to be called "osqueryd.exe".
|
|
var dstPath string
|
|
// check if we are adding extensions, which we namespace as "extensions/<ext_name>"
|
|
if strings.HasPrefix(name, "extensions/") {
|
|
dstPath = filepath.Join(name, platform, tag, strings.TrimPrefix(name, "extensions/"))
|
|
} else {
|
|
dstPath = filepath.Join(name, platform, tag, name)
|
|
}
|
|
switch {
|
|
case name == "desktop" && platform == "windows":
|
|
// This is a special case for the desktop target on Windows.
|
|
dstPath = filepath.Join(filepath.Dir(dstPath), constant.DesktopAppExecName+".exe")
|
|
case name == "desktop" && platform == "linux":
|
|
// This is a special case for the desktop target on Linux.
|
|
dstPath += ".tar.gz"
|
|
case strings.HasSuffix(target, ".exe"):
|
|
dstPath += ".exe"
|
|
case strings.HasSuffix(target, ".app.tar.gz"):
|
|
dstPath += ".app.tar.gz"
|
|
// osquery extensions require the .ext suffix
|
|
case strings.HasSuffix(target, ".ext"):
|
|
dstPath += ".ext"
|
|
}
|
|
fullPath := filepath.Join(targetsPath, dstPath)
|
|
paths = append(paths, dstPath)
|
|
if err := copyTarget(target, fullPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
type customMetadata struct {
|
|
Version string `json:"version"`
|
|
}
|
|
meta, err := json.Marshal(customMetadata{Version: version})
|
|
if err != nil {
|
|
return fmt.Errorf("marshal custom metadata: %w", err)
|
|
}
|
|
|
|
if err := repo.AddTargetsWithExpires(
|
|
paths,
|
|
meta,
|
|
time.Now().Add(targetsExpirationDuration),
|
|
); err != nil {
|
|
return fmt.Errorf("add targets: %w", err)
|
|
}
|
|
|
|
if err := repo.SnapshotWithExpires(time.Now().Add(snapshotExpirationDuration)); err != nil {
|
|
return fmt.Errorf("make snapshot: %w", err)
|
|
}
|
|
|
|
if err := repo.TimestampWithExpires(time.Now().Add(timestampExpirationDuration)); err != nil {
|
|
return fmt.Errorf("make timestamp: %w", err)
|
|
}
|
|
|
|
if err := repo.Commit(); err != nil {
|
|
return fmt.Errorf("commit repo: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func updatesTimestampCommand() *cli.Command {
|
|
return &cli.Command{
|
|
Name: "timestamp",
|
|
Usage: "Sign a new timestamp manifest",
|
|
Flags: updatesFlags(),
|
|
Action: updatesTimestampFunc,
|
|
}
|
|
}
|
|
|
|
func updatesTimestampFunc(c *cli.Context) error {
|
|
repo, err := openRepo(c.String("path"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := checkKeys(c.String("path"),
|
|
"timestamp",
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := repo.TimestampWithExpires(
|
|
time.Now().Add(timestampExpirationDuration),
|
|
); err != nil {
|
|
return fmt.Errorf("make timestamp: %w", err)
|
|
}
|
|
|
|
if err := repo.Commit(); err != nil {
|
|
return fmt.Errorf("commit repo: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func updatesRotateCommand() *cli.Command {
|
|
return &cli.Command{
|
|
Name: "rotate",
|
|
Usage: "Rotate signing keys",
|
|
ArgsUsage: "<role>",
|
|
Description: `Rotate the signing keys used for updates metadata signing. This should be used when keys are compromised or expiring.
|
|
|
|
role must be one of ['root', 'targets', 'timestamp', 'snapshot']
|
|
`,
|
|
Flags: updatesFlags(),
|
|
Action: updatesRotateFunc,
|
|
}
|
|
}
|
|
|
|
func updatesRotateFunc(c *cli.Context) error {
|
|
if c.NArg() != 1 {
|
|
return errors.New("role must be provided")
|
|
}
|
|
role := c.Args().Get(0)
|
|
|
|
repoPath := c.String("path")
|
|
repo, err := openRepo(repoPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
store, err := openLocalStore(repoPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := checkKeys(repoPath,
|
|
"root",
|
|
"targets",
|
|
"snapshot",
|
|
"timestamp",
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get old keys for role
|
|
keys, err := store.GetSigners(role)
|
|
if err != nil {
|
|
return fmt.Errorf("get keys for role: %w", err)
|
|
}
|
|
|
|
// Prepare to roll back in case of error.
|
|
success := false
|
|
commit, rollback, err := startRotatePseudoTx(repoPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if success {
|
|
if err := commit(); err != nil {
|
|
fmt.Println("Warning: failure during commit:", err)
|
|
}
|
|
} else {
|
|
fmt.Println("Rolling back changes.")
|
|
if err := rollback(); err != nil {
|
|
fmt.Println("Warning: failure during rollback:", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Delete old keys for role
|
|
for _, key := range keys {
|
|
id := key.PublicData().IDs()[0]
|
|
err := repo.RevokeKeyWithExpires(role, id, time.Now().Add(rootExpirationDuration))
|
|
if err != nil {
|
|
// go-tuf keeps keys around even after they are revoked from the manifest. We can skip
|
|
// tuf.ErrKeyNotFound as these represent keys that are not present in the manifest and
|
|
// so do not need to be revoked.
|
|
if !errors.As(err, &tuf.ErrKeyNotFound{}) {
|
|
return fmt.Errorf("revoke key: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO change passphrase for new key:
|
|
// Waiting on https://github.com/theupdateframework/go-tuf/pull/163
|
|
|
|
// Generate new key for role
|
|
if err := updatesGenKey(repo, role); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Re-sign the root metadata
|
|
if err := repo.Sign("root.json"); err != nil {
|
|
return fmt.Errorf("sign root.json: %w", err)
|
|
}
|
|
|
|
// Generate new metadata for each role (technically some of these may not need regeneration
|
|
// depending on which key was rotated, but there should be no harm in generating new ones for each).
|
|
if err := repo.AddTargetsWithExpires(nil, nil, time.Now().Add(targetsExpirationDuration)); err != nil {
|
|
return fmt.Errorf("generate targets: %w", err)
|
|
}
|
|
|
|
if err := repo.SnapshotWithExpires(time.Now().Add(snapshotExpirationDuration)); err != nil {
|
|
return fmt.Errorf("generate snapshot: %w", err)
|
|
}
|
|
|
|
if err := repo.TimestampWithExpires(time.Now().Add(timestampExpirationDuration)); err != nil {
|
|
return fmt.Errorf("generate timestamp: %w", err)
|
|
}
|
|
|
|
// Commit the changes.
|
|
if err := repo.Commit(); err != nil {
|
|
return fmt.Errorf("commit repo: %w", err)
|
|
}
|
|
|
|
success = true
|
|
return nil
|
|
}
|
|
|
|
// startRotatePseudoTx starts a "transaction" for the rotation routine, preparing a commit and
|
|
// rollback function for the metadata files that are modified by the rotation process.
|
|
func startRotatePseudoTx(repoPath string) (commit, rollback func() error, err error) {
|
|
repositoryDir := filepath.Join(repoPath, "repository")
|
|
if err := createBackups(repositoryDir); err != nil {
|
|
return nil, nil, fmt.Errorf("backup repository: %w", err)
|
|
}
|
|
keysDir := filepath.Join(repoPath, "keys")
|
|
if err := createBackups(keysDir); err != nil {
|
|
return nil, nil, fmt.Errorf("backup keys: %w", err)
|
|
}
|
|
|
|
commit = func() error {
|
|
// Remove the backups on successful rotation.
|
|
if err := os.RemoveAll(filepath.Join(repositoryDir, backupDirectory)); err != nil {
|
|
return fmt.Errorf("remove repository backup directory: %w", err)
|
|
}
|
|
if err := os.RemoveAll(filepath.Join(keysDir, backupDirectory)); err != nil {
|
|
return fmt.Errorf("remove keys backup directory: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
rollback = func() error {
|
|
// Restore the backups on failure.
|
|
if err := restoreBackups(repositoryDir); err != nil {
|
|
return fmt.Errorf("restore repository backup: %w", err)
|
|
}
|
|
if err := restoreBackups(keysDir); err != nil {
|
|
return fmt.Errorf("restore keys backup: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return commit, rollback, nil
|
|
}
|
|
|
|
// createBackups creates backups for metadata and key files during the key rotation process,
|
|
// allowing for rollback if necessary.
|
|
func createBackups(dirPath string) error {
|
|
// Only *.json files need to be backed up (other files are not modified)
|
|
backupPath := filepath.Join(dirPath, backupDirectory)
|
|
if err := os.Mkdir(backupPath, os.ModeDir|0o744); err != nil {
|
|
if errors.Is(err, fs.ErrExist) {
|
|
return fmt.Errorf("backup directory already exists: %w", err)
|
|
}
|
|
return fmt.Errorf("create backup directory: %w", err)
|
|
}
|
|
|
|
// Copy each of the *.json files into a backup file.
|
|
files, err := filepath.Glob(filepath.Join(dirPath, "*.json"))
|
|
if err != nil {
|
|
return fmt.Errorf("glob for backup: %w", err)
|
|
}
|
|
for _, path := range files {
|
|
if err := file.CopyWithPerms(
|
|
path,
|
|
filepath.Join(backupPath, filepath.Base(path)),
|
|
); err != nil {
|
|
return fmt.Errorf("copy for backup: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// restoreBackups restores the directory from the backups created by createBackups.
|
|
func restoreBackups(dirPath string) error {
|
|
backupDir := filepath.Join(dirPath, backupDirectory)
|
|
info, err := os.Stat(backupDir)
|
|
if err != nil {
|
|
return fmt.Errorf("stat backup path: %w", err)
|
|
}
|
|
if !info.IsDir() {
|
|
return fmt.Errorf("backup is not directory: %s", backupDir)
|
|
}
|
|
|
|
// Remove files that did not exist at backup time (determined by no corresponding backup).
|
|
files, err := filepath.Glob(filepath.Join(dirPath, "*.json"))
|
|
if err != nil {
|
|
return fmt.Errorf("glob for restore: %w", err)
|
|
}
|
|
for _, path := range files {
|
|
backupPath := filepath.Join(backupDir, filepath.Base(path))
|
|
exists, err := file.Exists(backupPath)
|
|
if err != nil {
|
|
return fmt.Errorf("check exists for restore: %w", err)
|
|
}
|
|
|
|
// File does not exist in the backup, remove it because this implies that the file was added
|
|
// since the backup was taken.
|
|
if !exists {
|
|
if err := os.Remove(path); err != nil {
|
|
return fmt.Errorf("remove for restore: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Restore files from backups.
|
|
backupFiles, err := filepath.Glob(filepath.Join(backupDir, "*.json"))
|
|
if err != nil {
|
|
return fmt.Errorf("glob for restore: %w", err)
|
|
}
|
|
for _, path := range backupFiles {
|
|
originalPath := filepath.Join(dirPath, filepath.Base(path))
|
|
|
|
// Replace with the backed up file, copying the previous permissions.
|
|
if err := file.CopyWithPerms(path, originalPath); err != nil {
|
|
return fmt.Errorf("copy for restore: %w", err)
|
|
}
|
|
}
|
|
|
|
// Remove the backups now that we are finished with the restore.
|
|
if err := os.RemoveAll(backupDir); err != nil {
|
|
return fmt.Errorf("remove backup directory: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func checkKeys(repoPath string, keys ...string) error {
|
|
// Verify we can decrypt necessary role keys
|
|
store := tuf.FileSystemStore(repoPath, passHandler.getPassphrase)
|
|
for _, role := range keys {
|
|
if err := passHandler.checkPassphrase(store, role); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func copyTarget(srcPath, dstPath string) error {
|
|
src, err := os.Open(srcPath)
|
|
if err != nil {
|
|
return fmt.Errorf("open src for copy: %w", err)
|
|
}
|
|
defer src.Close()
|
|
|
|
if err := secure.MkdirAll(filepath.Dir(dstPath), 0o700); err != nil {
|
|
return fmt.Errorf("create dst dir for copy: %w", err)
|
|
}
|
|
|
|
dst, err := secure.OpenFile(dstPath, os.O_RDWR|os.O_CREATE, 0o600)
|
|
if err != nil {
|
|
return fmt.Errorf("open dst for copy: %w", err)
|
|
}
|
|
defer dst.Close()
|
|
|
|
if _, err := io.Copy(dst, src); err != nil {
|
|
return fmt.Errorf("copy src to dst: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func updatesGenKey(repo *tuf.Repo, role string) error {
|
|
keyids, err := repo.GenKeyWithExpires(role, time.Now().Add(keyExpirationDuration))
|
|
if err != nil {
|
|
return fmt.Errorf("generate %s key: %w", role, err)
|
|
}
|
|
|
|
if len(keyids) != 1 {
|
|
return fmt.Errorf("expected 1 keyid for %s key: got %d", role, len(keyids))
|
|
}
|
|
fmt.Printf("Generated %s key with ID: %s\n", role, keyids[0])
|
|
|
|
return nil
|
|
}
|
|
|
|
func openLocalStore(path string) (tuf.LocalStore, error) {
|
|
store := tuf.FileSystemStore(path, passHandler.getPassphrase)
|
|
meta, err := store.GetMeta()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get repo meta: %w", err)
|
|
}
|
|
if len(meta) == 0 {
|
|
return nil, fmt.Errorf("repo not initialized: %s", path)
|
|
}
|
|
return store, nil
|
|
}
|
|
|
|
func openRepo(path string) (*tuf.Repo, error) {
|
|
store, err := openLocalStore(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
repo, err := tuf.NewRepo(store)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("new repo from store: %w", err)
|
|
}
|
|
|
|
return repo, nil
|
|
}
|
|
|
|
// passphraseHandler will cache passphrases so that they can be checked prior to
|
|
// usage without requiring the user to enter them more than once.
|
|
type passphraseHandler struct {
|
|
cache map[string][]byte
|
|
}
|
|
|
|
func newPassphraseHandler() *passphraseHandler {
|
|
return &passphraseHandler{cache: make(map[string][]byte)}
|
|
}
|
|
|
|
// TODO #4145 make use of recently added `change` argument
|
|
func (p *passphraseHandler) getPassphrase(role string, confirm, change bool) ([]byte, error) {
|
|
// Check cache
|
|
if pass, ok := p.cache[role]; ok {
|
|
return pass, nil
|
|
}
|
|
|
|
// Get passphrase
|
|
var err error
|
|
passphrase, err := p.readPassphrase(role, confirm)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(passphrase) == 0 {
|
|
return nil, errors.New("passphrase must not be empty")
|
|
}
|
|
|
|
// Store cache
|
|
p.cache[role] = passphrase
|
|
|
|
return passphrase, nil
|
|
}
|
|
|
|
// export FLEET_TIMESTAMP_PASSPHRASE=insecure FLEET_SNAPSHOT_PASSPHRASE=insecure FLEET_TARGETS_PASSPHRASE=insecure FLEET_ROOT_PASSPHASE=insecure
|
|
func (p *passphraseHandler) passphraseEnvName(role string) string {
|
|
return fmt.Sprintf("FLEET_%s_PASSPHRASE", strings.ToUpper(role))
|
|
}
|
|
|
|
func (p *passphraseHandler) getPassphraseFromEnv(role string) []byte {
|
|
if pass, ok := os.LookupEnv(p.passphraseEnvName(role)); ok {
|
|
return []byte(pass)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Read input. Adapted from
|
|
// https://github.com/theupdateframework/go-tuf/blob/aee6270feb5596036edde4b6d7564fa17db811cb/cmd/tuf/main.go#L125
|
|
func (p *passphraseHandler) readPassphrase(role string, confirm bool) ([]byte, error) {
|
|
// Loop until error reading or successful confirmation (if needed)
|
|
for {
|
|
if passphrase := p.getPassphraseFromEnv(role); passphrase != nil {
|
|
return passphrase, nil
|
|
}
|
|
|
|
fmt.Printf("Enter %s key passphrase: ", role)
|
|
// the int(...) conversion is required as on Windows syscall.Stdin is of type Handle.
|
|
passphrase, err := terminal.ReadPassword(int(syscall.Stdin)) //nolint:unconvert
|
|
fmt.Println()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read password: %w", err)
|
|
}
|
|
|
|
if !confirm {
|
|
return passphrase, nil
|
|
}
|
|
|
|
fmt.Printf("Repeat %s key passphrase: ", role)
|
|
// the int(...) conversion is required as on Windows syscall.Stdin is of type Handle.
|
|
confirmation, err := terminal.ReadPassword(int(syscall.Stdin)) //nolint:unconvert
|
|
fmt.Println()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read password confirmation: %w", err)
|
|
}
|
|
|
|
if bytes.Equal(passphrase, confirmation) {
|
|
return passphrase, nil
|
|
}
|
|
|
|
fmt.Println("The entered passphrases do not match")
|
|
}
|
|
}
|
|
|
|
func (p *passphraseHandler) checkPassphrase(store tuf.LocalStore, role string) error {
|
|
// It seems the only way to check the passphrase is to try decrypting the
|
|
// key and see if it is successful. Loop until successful decryption or
|
|
// non-decryption error.
|
|
for {
|
|
keys, err := store.GetSigners(role)
|
|
if err != nil {
|
|
// TODO it would be helpful if we could upstream a new error type in
|
|
// go-tuf and use errors.Is instead of comparing the text of the
|
|
// error as we do currently.
|
|
if ctxerr.Cause(err).Error() != decryptionFailedError {
|
|
return err
|
|
} else if err != nil {
|
|
if p.getPassphraseFromEnv(role) != nil {
|
|
// Fatal error if environment variable passphrase is
|
|
// incorrect
|
|
return fmt.Errorf("%s passphrase from %s is invalid", role, p.passphraseEnvName(role))
|
|
}
|
|
|
|
fmt.Printf("Failed to decrypt %s key. Try again.\n", role)
|
|
delete(p.cache, role)
|
|
}
|
|
continue
|
|
} else if len(keys) == 0 {
|
|
return fmt.Errorf("%s key not found", role)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|