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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
|
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package cli provides the skeleton of a CLI for building release packages.
package cli
import (
"context"
"encoding/binary"
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/clientupdate/distsign"
"tailscale.com/release/dist"
)
// CLI returns a CLI root command to build release packages.
//
// getTargets is a function that gets run in the Exec function of commands that
// need to know the target list. Its execution is deferred in this way to allow
// customization of command FlagSets with flags that influence the target list.
func CLI(getTargets func() ([]dist.Target, error)) *ffcli.Command {
return &ffcli.Command{
Name: "dist",
ShortUsage: "dist [flags] <command> [command flags]",
ShortHelp: "Build tailscale release packages for distribution",
LongHelp: `For help on subcommands, add --help after: "dist list --help".`,
Subcommands: []*ffcli.Command{
{
Name: "list",
Exec: func(ctx context.Context, args []string) error {
targets, err := getTargets()
if err != nil {
return err
}
return runList(ctx, args, targets)
},
ShortUsage: "dist list [target filters]",
ShortHelp: "List all available release targets.",
LongHelp: strings.TrimSpace(`
If filters are provided, only targets matching at least one filter are listed.
Filters can use glob patterns (* and ?).
`),
},
{
Name: "build",
Exec: func(ctx context.Context, args []string) error {
targets, err := getTargets()
if err != nil {
return err
}
return runBuild(ctx, args, targets)
},
ShortUsage: "dist build [target filters]",
ShortHelp: "Build release files",
FlagSet: (func() *flag.FlagSet {
fs := flag.NewFlagSet("build", flag.ExitOnError)
fs.StringVar(&buildArgs.manifest, "manifest", "", "manifest file to write")
fs.BoolVar(&buildArgs.verbose, "verbose", false, "verbose logging")
fs.StringVar(&buildArgs.webClientRoot, "web-client-root", "", "path to root of web client source to build")
fs.StringVar(&buildArgs.outPath, "out", "", "path to write output artifacts (defaults to '$PWD/dist' if not set)")
return fs
})(),
LongHelp: strings.TrimSpace(`
If filters are provided, only targets matching at least one filter are built.
Filters can use glob patterns (* and ?).
`),
},
{
Name: "gen-key",
Exec: func(ctx context.Context, args []string) error {
return runGenKey(ctx)
},
ShortUsage: "dist gen-key",
ShortHelp: "Generate root or signing key pair",
FlagSet: (func() *flag.FlagSet {
fs := flag.NewFlagSet("gen-key", flag.ExitOnError)
fs.BoolVar(&genKeyArgs.root, "root", false, "generate a root key")
fs.BoolVar(&genKeyArgs.signing, "signing", false, "generate a signing key")
fs.StringVar(&genKeyArgs.privPath, "priv-path", "private-key.pem", "output path for the private key")
fs.StringVar(&genKeyArgs.pubPath, "pub-path", "public-key.pem", "output path for the public key")
return fs
})(),
},
{
Name: "sign-key",
Exec: func(ctx context.Context, args []string) error {
return runSignKey(ctx)
},
ShortUsage: "dist sign-key",
ShortHelp: "Sign signing keys with a root key",
FlagSet: (func() *flag.FlagSet {
fs := flag.NewFlagSet("sign-key", flag.ExitOnError)
fs.StringVar(&signKeyArgs.rootPrivPath, "root-priv-path", "root-private-key.pem", "path to the root private key to sign with")
fs.StringVar(&signKeyArgs.signPubPath, "sign-pub-path", "signing-public-keys.pem", "path to the signing public key bundle to sign; the bundle should include all active signing keys")
fs.StringVar(&signKeyArgs.sigPath, "sig-path", "signature.bin", "oputput path for the signature")
return fs
})(),
},
{
Name: "verify-key-signature",
Exec: func(ctx context.Context, args []string) error {
return runVerifyKeySignature(ctx)
},
ShortUsage: "dist verify-key-signature",
ShortHelp: "Verify a root signture of the signing keys' bundle",
FlagSet: (func() *flag.FlagSet {
fs := flag.NewFlagSet("verify-key-signature", flag.ExitOnError)
fs.StringVar(&verifyKeySignatureArgs.rootPubPath, "root-pub-path", "root-public-key.pem", "path to the root public key; this can be a bundle of multiple keys")
fs.StringVar(&verifyKeySignatureArgs.signPubPath, "sign-pub-path", "", "path to the signing public key bundle that was signed")
fs.StringVar(&verifyKeySignatureArgs.sigPath, "sig-path", "signature.bin", "path to the signature file")
return fs
})(),
},
{
Name: "verify-package-signature",
Exec: func(ctx context.Context, args []string) error {
return runVerifyPackageSignature(ctx)
},
ShortUsage: "dist verify-package-signature",
ShortHelp: "Verify a package signture using a signing key",
FlagSet: (func() *flag.FlagSet {
fs := flag.NewFlagSet("verify-package-signature", flag.ExitOnError)
fs.StringVar(&verifyPackageSignatureArgs.signPubPath, "sign-pub-path", "signing-public-key.pem", "path to the signing public key; this can be a bundle of multiple keys")
fs.StringVar(&verifyPackageSignatureArgs.packagePath, "package-path", "", "path to the package that was signed")
fs.StringVar(&verifyPackageSignatureArgs.sigPath, "sig-path", "signature.bin", "path to the signature file")
return fs
})(),
},
},
Exec: func(context.Context, []string) error { return flag.ErrHelp },
}
}
func runList(ctx context.Context, filters []string, targets []dist.Target) error {
if len(filters) == 0 {
filters = []string{"all"}
}
tgts, err := dist.FilterTargets(targets, filters)
if err != nil {
return err
}
for _, tgt := range tgts {
fmt.Println(tgt)
}
return nil
}
var buildArgs struct {
manifest string
verbose bool
webClientRoot string
outPath string
}
func runBuild(ctx context.Context, filters []string, targets []dist.Target) error {
tgts, err := dist.FilterTargets(targets, filters)
if err != nil {
return err
}
if len(tgts) == 0 {
return errors.New("no targets matched (did you mean 'dist build all'?)")
}
st := time.Now()
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("getting working directory: %w", err)
}
outPath := filepath.Join(wd, "dist")
if buildArgs.outPath != "" {
outPath = buildArgs.outPath
}
b, err := dist.NewBuild(wd, outPath)
if err != nil {
return fmt.Errorf("creating build context: %w", err)
}
defer b.Close()
b.Verbose = buildArgs.verbose
b.WebClientSource = buildArgs.webClientRoot
out, err := b.Build(tgts)
if err != nil {
return fmt.Errorf("building targets: %w", err)
}
if buildArgs.manifest != "" {
// Make the built paths relative to the manifest file.
manifest, err := filepath.Abs(buildArgs.manifest)
if err != nil {
return fmt.Errorf("getting absolute path of manifest: %w", err)
}
for i := range out {
if !filepath.IsAbs(out[i]) {
out[i] = filepath.Join(b.Out, out[i])
}
rel, err := filepath.Rel(filepath.Dir(manifest), out[i])
if err != nil {
return fmt.Errorf("making path relative: %w", err)
}
out[i] = rel
}
if err := os.WriteFile(manifest, []byte(strings.Join(out, "\n")), 0644); err != nil {
return fmt.Errorf("writing manifest: %w", err)
}
}
fmt.Println("Done! Took", time.Since(st))
return nil
}
var genKeyArgs struct {
root bool
signing bool
privPath string
pubPath string
}
func runGenKey(ctx context.Context) error {
var pub, priv []byte
var err error
switch {
case genKeyArgs.root && genKeyArgs.signing:
return errors.New("only one of --root or --signing can be set")
case !genKeyArgs.root && !genKeyArgs.signing:
return errors.New("set either --root or --signing")
case genKeyArgs.root:
priv, pub, err = distsign.GenerateRootKey()
case genKeyArgs.signing:
priv, pub, err = distsign.GenerateSigningKey()
}
if err != nil {
return err
}
if err := os.WriteFile(genKeyArgs.privPath, priv, 0400); err != nil {
return fmt.Errorf("failed writing private key: %w", err)
}
fmt.Println("wrote private key to", genKeyArgs.privPath)
if err := os.WriteFile(genKeyArgs.pubPath, pub, 0400); err != nil {
return fmt.Errorf("failed writing public key: %w", err)
}
fmt.Println("wrote public key to", genKeyArgs.pubPath)
return nil
}
var signKeyArgs struct {
rootPrivPath string
signPubPath string
sigPath string
}
func runSignKey(ctx context.Context) error {
rkRaw, err := os.ReadFile(signKeyArgs.rootPrivPath)
if err != nil {
return err
}
rk, err := distsign.ParseRootKey(rkRaw)
if err != nil {
return err
}
bundle, err := os.ReadFile(signKeyArgs.signPubPath)
if err != nil {
return err
}
sig, err := rk.SignSigningKeys(bundle)
if err != nil {
return err
}
if err := os.WriteFile(signKeyArgs.sigPath, sig, 0400); err != nil {
return fmt.Errorf("failed writing signature file: %w", err)
}
fmt.Println("wrote signature to", signKeyArgs.sigPath)
return nil
}
var verifyKeySignatureArgs struct {
rootPubPath string
signPubPath string
sigPath string
}
func runVerifyKeySignature(ctx context.Context) error {
args := verifyKeySignatureArgs
rootPubBundle, err := os.ReadFile(args.rootPubPath)
if err != nil {
return err
}
rootPubs, err := distsign.ParseRootKeyBundle(rootPubBundle)
if err != nil {
return fmt.Errorf("parsing %q: %w", args.rootPubPath, err)
}
signPubBundle, err := os.ReadFile(args.signPubPath)
if err != nil {
return err
}
sig, err := os.ReadFile(args.sigPath)
if err != nil {
return err
}
if !distsign.VerifyAny(rootPubs, signPubBundle, sig) {
return errors.New("signature not valid")
}
fmt.Println("signature ok")
return nil
}
var verifyPackageSignatureArgs struct {
signPubPath string
packagePath string
sigPath string
}
func runVerifyPackageSignature(ctx context.Context) error {
args := verifyPackageSignatureArgs
signPubBundle, err := os.ReadFile(args.signPubPath)
if err != nil {
return err
}
signPubs, err := distsign.ParseSigningKeyBundle(signPubBundle)
if err != nil {
return fmt.Errorf("parsing %q: %w", args.signPubPath, err)
}
pkg, err := os.Open(args.packagePath)
if err != nil {
return err
}
defer pkg.Close()
pkgHash := distsign.NewPackageHash()
if _, err := io.Copy(pkgHash, pkg); err != nil {
return fmt.Errorf("reading %q: %w", args.packagePath, err)
}
hash := binary.LittleEndian.AppendUint64(pkgHash.Sum(nil), uint64(pkgHash.Len()))
sig, err := os.ReadFile(args.sigPath)
if err != nil {
return err
}
if !distsign.VerifyAny(signPubs, hash, sig) {
return errors.New("signature not valid")
}
fmt.Println("signature ok")
return nil
}
|