summaryrefslogtreecommitdiffstats
path: root/cmd/tuf/main.go
blob: dc6a256c5eb71feea2dbf9e575d1f4cc879ede0e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package main

import (
	"bytes"
	"errors"
	"fmt"
	"log"
	"os"
	"strconv"
	"strings"
	"syscall"
	"time"

	docopt "github.com/flynn/go-docopt"
	tuf "github.com/theupdateframework/go-tuf"
	"github.com/theupdateframework/go-tuf/util"
	"golang.org/x/term"
)

func main() {
	log.SetFlags(0)

	usage := `usage: tuf [-h|--help] [-d|--dir=<dir>] [--insecure-plaintext] <command> [<args>...]

Options:
  -h, --help
  -d <dir>              The path to the repository (defaults to the current working directory)
  --insecure-plaintext  Don't encrypt signing keys

Commands:
  help               Show usage for a specific command
  init               Initialize a new repository
  gen-key            Generate a new signing key for a specific metadata file
  revoke-key         Revoke a signing key
  add                Add target file(s)
  remove             Remove a target file
  snapshot           Update the snapshot metadata file
  timestamp          Update the timestamp metadata file
  payload            Output a role's metadata file for signing
  add-signatures     Adds signatures generated offline
  sign               Sign a role's metadata file
  sign-payload       Sign a file from the "payload" command.
  status             Check if a role's metadata has expired
  commit             Commit staged files to the repository
  regenerate         Recreate the targets metadata file [Not supported yet]
  set-threshold      Sets the threshold for a role
  get-threshold      Outputs the threshold for a role
  change-passphrase  Changes the passphrase for given role keys file
  root-keys          Output a JSON serialized array of root keys to STDOUT
  clean              Remove all staged metadata files

See "tuf help <command>" for more information on a specific command
`

	args, _ := docopt.Parse(usage, nil, true, "", true)
	cmd := args.String["<command>"]
	cmdArgs := args.All["<args>"].([]string)

	if cmd == "help" {
		if len(cmdArgs) == 0 { // `tuf help`
			fmt.Fprint(os.Stdout, usage)
			return
		} else { // `tuf help <command>`
			cmd = cmdArgs[0]
			cmdArgs = []string{"--help"}
		}
	}

	dir, ok := args.String["-d"]
	if !ok {
		dir = args.String["--dir"]
	}
	if dir == "" {
		var err error
		dir, err = os.Getwd()
		if err != nil {
			log.Fatal(err)
		}
	}

	if err := runCommand(cmd, cmdArgs, dir, args.Bool["--insecure-plaintext"]); err != nil {
		log.Fatalln("ERROR:", err)
	}
}

type cmdFunc func(*docopt.Args, *tuf.Repo) error

type command struct {
	usage string
	f     cmdFunc
}

var commands = make(map[string]*command)

func register(name string, f cmdFunc, usage string) {
	commands[name] = &command{usage: usage, f: f}
}

func runCommand(name string, args []string, dir string, insecure bool) error {
	argv := make([]string, 1, 1+len(args))
	argv[0] = name
	argv = append(argv, args...)

	cmd, ok := commands[name]
	if !ok {
		return fmt.Errorf("%s is not a tuf command. See 'tuf help'", name)
	}

	parsedArgs, err := docopt.Parse(cmd.usage, argv, true, "", true)
	if err != nil {
		return err
	}

	var p util.PassphraseFunc
	if !insecure {
		p = getPassphrase
	}
	logger := log.New(os.Stdout, "", 0)
	storeOpts := tuf.StoreOpts{Logger: logger, PassFunc: p}

	repo, err := tuf.NewRepoWithOpts(tuf.FileSystemStoreWithOpts(dir, storeOpts),
		tuf.WithLogger(logger))
	if err != nil {
		return err
	}
	return cmd.f(parsedArgs, repo)
}

func parseExpires(arg string) (time.Time, error) {
	days, err := strconv.Atoi(arg)
	if err != nil {
		return time.Time{}, fmt.Errorf("failed to parse --expires arg: %s", err)
	}
	return time.Now().AddDate(0, 0, days).UTC(), nil
}

func getPassphrase(role string, confirm bool, change bool) ([]byte, error) {
	// In case of change we need to prompt explicitly for a new passphrase
	// and not read it from the environment variable, if present
	if pass := os.Getenv(fmt.Sprintf("TUF_%s_PASSPHRASE", strings.ToUpper(role))); pass != "" && !change {
		return []byte(pass), nil
	}
	// Alter role string if we are prompting for a passphrase change
	if change {
		// Check if environment variable for new passphrase exist
		if new_pass := os.Getenv(fmt.Sprintf("TUF_NEW_%s_PASSPHRASE", strings.ToUpper(role))); new_pass != "" {
			// If so, just read the new passphrase from it and return
			return []byte(new_pass), nil
		}
		// No environment variable set, so proceed prompting for new passphrase
		role = fmt.Sprintf("new %s", role)
	}
	fmt.Fprintf(os.Stderr, "Enter %s keys passphrase: ", role)
	passphrase, err := term.ReadPassword(int(syscall.Stdin))
	fmt.Fprintln(os.Stderr)
	if err != nil {
		return nil, err
	}

	if !confirm {
		return passphrase, nil
	}

	fmt.Fprintf(os.Stderr, "Repeat %s keys passphrase: ", role)
	confirmation, err := term.ReadPassword(int(syscall.Stdin))
	fmt.Fprintln(os.Stderr)
	if err != nil {
		return nil, err
	}

	if !bytes.Equal(passphrase, confirmation) {
		return nil, errors.New("the entered passphrases do not match")
	}
	return passphrase, nil
}