cli.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. // Package cli provides the skeleton of a CLI for building release packages.
  4. package cli
  5. import (
  6. "context"
  7. "encoding/binary"
  8. "errors"
  9. "flag"
  10. "fmt"
  11. "io"
  12. "os"
  13. "path/filepath"
  14. "strings"
  15. "time"
  16. "github.com/peterbourgon/ff/v3/ffcli"
  17. "tailscale.com/clientupdate/distsign"
  18. "tailscale.com/release/dist"
  19. )
  20. // CLI returns a CLI root command to build release packages.
  21. //
  22. // getTargets is a function that gets run in the Exec function of commands that
  23. // need to know the target list. Its execution is deferred in this way to allow
  24. // customization of command FlagSets with flags that influence the target list.
  25. func CLI(getTargets func() ([]dist.Target, error)) *ffcli.Command {
  26. return &ffcli.Command{
  27. Name: "dist",
  28. ShortUsage: "dist [flags] <command> [command flags]",
  29. ShortHelp: "Build tailscale release packages for distribution",
  30. LongHelp: `For help on subcommands, add --help after: "dist list --help".`,
  31. Subcommands: []*ffcli.Command{
  32. {
  33. Name: "list",
  34. Exec: func(ctx context.Context, args []string) error {
  35. targets, err := getTargets()
  36. if err != nil {
  37. return err
  38. }
  39. return runList(ctx, args, targets)
  40. },
  41. ShortUsage: "dist list [target filters]",
  42. ShortHelp: "List all available release targets.",
  43. LongHelp: strings.TrimSpace(`
  44. If filters are provided, only targets matching at least one filter are listed.
  45. Filters can use glob patterns (* and ?).
  46. `),
  47. },
  48. {
  49. Name: "build",
  50. Exec: func(ctx context.Context, args []string) error {
  51. targets, err := getTargets()
  52. if err != nil {
  53. return err
  54. }
  55. return runBuild(ctx, args, targets)
  56. },
  57. ShortUsage: "dist build [target filters]",
  58. ShortHelp: "Build release files",
  59. FlagSet: (func() *flag.FlagSet {
  60. fs := flag.NewFlagSet("build", flag.ExitOnError)
  61. fs.StringVar(&buildArgs.manifest, "manifest", "", "manifest file to write")
  62. fs.BoolVar(&buildArgs.verbose, "verbose", false, "verbose logging")
  63. fs.StringVar(&buildArgs.webClientRoot, "web-client-root", "", "path to root of web client source to build")
  64. return fs
  65. })(),
  66. LongHelp: strings.TrimSpace(`
  67. If filters are provided, only targets matching at least one filter are built.
  68. Filters can use glob patterns (* and ?).
  69. `),
  70. },
  71. {
  72. Name: "gen-key",
  73. Exec: func(ctx context.Context, args []string) error {
  74. return runGenKey(ctx)
  75. },
  76. ShortUsage: "dist gen-key",
  77. ShortHelp: "Generate root or signing key pair",
  78. FlagSet: (func() *flag.FlagSet {
  79. fs := flag.NewFlagSet("gen-key", flag.ExitOnError)
  80. fs.BoolVar(&genKeyArgs.root, "root", false, "generate a root key")
  81. fs.BoolVar(&genKeyArgs.signing, "signing", false, "generate a signing key")
  82. fs.StringVar(&genKeyArgs.privPath, "priv-path", "private-key.pem", "output path for the private key")
  83. fs.StringVar(&genKeyArgs.pubPath, "pub-path", "public-key.pem", "output path for the public key")
  84. return fs
  85. })(),
  86. },
  87. {
  88. Name: "sign-key",
  89. Exec: func(ctx context.Context, args []string) error {
  90. return runSignKey(ctx)
  91. },
  92. ShortUsage: "dist sign-key",
  93. ShortHelp: "Sign signing keys with a root key",
  94. FlagSet: (func() *flag.FlagSet {
  95. fs := flag.NewFlagSet("sign-key", flag.ExitOnError)
  96. fs.StringVar(&signKeyArgs.rootPrivPath, "root-priv-path", "root-private-key.pem", "path to the root private key to sign with")
  97. 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")
  98. fs.StringVar(&signKeyArgs.sigPath, "sig-path", "signature.bin", "oputput path for the signature")
  99. return fs
  100. })(),
  101. },
  102. {
  103. Name: "verify-key-signature",
  104. Exec: func(ctx context.Context, args []string) error {
  105. return runVerifyKeySignature(ctx)
  106. },
  107. ShortUsage: "dist verify-key-signature",
  108. ShortHelp: "Verify a root signture of the signing keys' bundle",
  109. FlagSet: (func() *flag.FlagSet {
  110. fs := flag.NewFlagSet("verify-key-signature", flag.ExitOnError)
  111. 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")
  112. fs.StringVar(&verifyKeySignatureArgs.signPubPath, "sign-pub-path", "", "path to the signing public key bundle that was signed")
  113. fs.StringVar(&verifyKeySignatureArgs.sigPath, "sig-path", "signature.bin", "path to the signature file")
  114. return fs
  115. })(),
  116. },
  117. {
  118. Name: "verify-package-signature",
  119. Exec: func(ctx context.Context, args []string) error {
  120. return runVerifyPackageSignature(ctx)
  121. },
  122. ShortUsage: "dist verify-package-signature",
  123. ShortHelp: "Verify a package signture using a signing key",
  124. FlagSet: (func() *flag.FlagSet {
  125. fs := flag.NewFlagSet("verify-package-signature", flag.ExitOnError)
  126. 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")
  127. fs.StringVar(&verifyPackageSignatureArgs.packagePath, "package-path", "", "path to the package that was signed")
  128. fs.StringVar(&verifyPackageSignatureArgs.sigPath, "sig-path", "signature.bin", "path to the signature file")
  129. return fs
  130. })(),
  131. },
  132. },
  133. Exec: func(context.Context, []string) error { return flag.ErrHelp },
  134. }
  135. }
  136. func runList(ctx context.Context, filters []string, targets []dist.Target) error {
  137. if len(filters) == 0 {
  138. filters = []string{"all"}
  139. }
  140. tgts, err := dist.FilterTargets(targets, filters)
  141. if err != nil {
  142. return err
  143. }
  144. for _, tgt := range tgts {
  145. fmt.Println(tgt)
  146. }
  147. return nil
  148. }
  149. var buildArgs struct {
  150. manifest string
  151. verbose bool
  152. webClientRoot string
  153. }
  154. func runBuild(ctx context.Context, filters []string, targets []dist.Target) error {
  155. tgts, err := dist.FilterTargets(targets, filters)
  156. if err != nil {
  157. return err
  158. }
  159. if len(tgts) == 0 {
  160. return errors.New("no targets matched (did you mean 'dist build all'?)")
  161. }
  162. st := time.Now()
  163. wd, err := os.Getwd()
  164. if err != nil {
  165. return fmt.Errorf("getting working directory: %w", err)
  166. }
  167. b, err := dist.NewBuild(wd, filepath.Join(wd, "dist"))
  168. if err != nil {
  169. return fmt.Errorf("creating build context: %w", err)
  170. }
  171. defer b.Close()
  172. b.Verbose = buildArgs.verbose
  173. b.WebClientSource = buildArgs.webClientRoot
  174. out, err := b.Build(tgts)
  175. if err != nil {
  176. return fmt.Errorf("building targets: %w", err)
  177. }
  178. if buildArgs.manifest != "" {
  179. // Make the built paths relative to the manifest file.
  180. manifest, err := filepath.Abs(buildArgs.manifest)
  181. if err != nil {
  182. return fmt.Errorf("getting absolute path of manifest: %w", err)
  183. }
  184. for i := range out {
  185. if !filepath.IsAbs(out[i]) {
  186. out[i] = filepath.Join(b.Out, out[i])
  187. }
  188. rel, err := filepath.Rel(filepath.Dir(manifest), out[i])
  189. if err != nil {
  190. return fmt.Errorf("making path relative: %w", err)
  191. }
  192. out[i] = rel
  193. }
  194. if err := os.WriteFile(manifest, []byte(strings.Join(out, "\n")), 0644); err != nil {
  195. return fmt.Errorf("writing manifest: %w", err)
  196. }
  197. }
  198. fmt.Println("Done! Took", time.Since(st))
  199. return nil
  200. }
  201. var genKeyArgs struct {
  202. root bool
  203. signing bool
  204. privPath string
  205. pubPath string
  206. }
  207. func runGenKey(ctx context.Context) error {
  208. var pub, priv []byte
  209. var err error
  210. switch {
  211. case genKeyArgs.root && genKeyArgs.signing:
  212. return errors.New("only one of --root or --signing can be set")
  213. case !genKeyArgs.root && !genKeyArgs.signing:
  214. return errors.New("set either --root or --signing")
  215. case genKeyArgs.root:
  216. priv, pub, err = distsign.GenerateRootKey()
  217. case genKeyArgs.signing:
  218. priv, pub, err = distsign.GenerateSigningKey()
  219. }
  220. if err != nil {
  221. return err
  222. }
  223. if err := os.WriteFile(genKeyArgs.privPath, priv, 0400); err != nil {
  224. return fmt.Errorf("failed writing private key: %w", err)
  225. }
  226. fmt.Println("wrote private key to", genKeyArgs.privPath)
  227. if err := os.WriteFile(genKeyArgs.pubPath, pub, 0400); err != nil {
  228. return fmt.Errorf("failed writing public key: %w", err)
  229. }
  230. fmt.Println("wrote public key to", genKeyArgs.pubPath)
  231. return nil
  232. }
  233. var signKeyArgs struct {
  234. rootPrivPath string
  235. signPubPath string
  236. sigPath string
  237. }
  238. func runSignKey(ctx context.Context) error {
  239. rkRaw, err := os.ReadFile(signKeyArgs.rootPrivPath)
  240. if err != nil {
  241. return err
  242. }
  243. rk, err := distsign.ParseRootKey(rkRaw)
  244. if err != nil {
  245. return err
  246. }
  247. bundle, err := os.ReadFile(signKeyArgs.signPubPath)
  248. if err != nil {
  249. return err
  250. }
  251. sig, err := rk.SignSigningKeys(bundle)
  252. if err != nil {
  253. return err
  254. }
  255. if err := os.WriteFile(signKeyArgs.sigPath, sig, 0400); err != nil {
  256. return fmt.Errorf("failed writing signature file: %w", err)
  257. }
  258. fmt.Println("wrote signature to", signKeyArgs.sigPath)
  259. return nil
  260. }
  261. var verifyKeySignatureArgs struct {
  262. rootPubPath string
  263. signPubPath string
  264. sigPath string
  265. }
  266. func runVerifyKeySignature(ctx context.Context) error {
  267. args := verifyKeySignatureArgs
  268. rootPubBundle, err := os.ReadFile(args.rootPubPath)
  269. if err != nil {
  270. return err
  271. }
  272. rootPubs, err := distsign.ParseRootKeyBundle(rootPubBundle)
  273. if err != nil {
  274. return fmt.Errorf("parsing %q: %w", args.rootPubPath, err)
  275. }
  276. signPubBundle, err := os.ReadFile(args.signPubPath)
  277. if err != nil {
  278. return err
  279. }
  280. sig, err := os.ReadFile(args.sigPath)
  281. if err != nil {
  282. return err
  283. }
  284. if !distsign.VerifyAny(rootPubs, signPubBundle, sig) {
  285. return errors.New("signature not valid")
  286. }
  287. fmt.Println("signature ok")
  288. return nil
  289. }
  290. var verifyPackageSignatureArgs struct {
  291. signPubPath string
  292. packagePath string
  293. sigPath string
  294. }
  295. func runVerifyPackageSignature(ctx context.Context) error {
  296. args := verifyPackageSignatureArgs
  297. signPubBundle, err := os.ReadFile(args.signPubPath)
  298. if err != nil {
  299. return err
  300. }
  301. signPubs, err := distsign.ParseSigningKeyBundle(signPubBundle)
  302. if err != nil {
  303. return fmt.Errorf("parsing %q: %w", args.signPubPath, err)
  304. }
  305. pkg, err := os.Open(args.packagePath)
  306. if err != nil {
  307. return err
  308. }
  309. defer pkg.Close()
  310. pkgHash := distsign.NewPackageHash()
  311. if _, err := io.Copy(pkgHash, pkg); err != nil {
  312. return fmt.Errorf("reading %q: %w", args.packagePath, err)
  313. }
  314. hash := binary.LittleEndian.AppendUint64(pkgHash.Sum(nil), uint64(pkgHash.Len()))
  315. sig, err := os.ReadFile(args.sigPath)
  316. if err != nil {
  317. return err
  318. }
  319. if !distsign.VerifyAny(signPubs, hash, sig) {
  320. return errors.New("signature not valid")
  321. }
  322. fmt.Println("signature ok")
  323. return nil
  324. }