2023-02-24 22:19:13 +00:00
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
2023-02-24 22:15:35 +00:00
// Package cli provides the skeleton of a CLI for building release packages.
package cli
import (
"context"
2023-07-31 22:47:00 +00:00
"crypto"
"crypto/x509"
"encoding/pem"
2023-02-24 22:15:35 +00:00
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/peterbourgon/ff/v3/ffcli"
2023-08-22 23:29:56 +00:00
"tailscale.com/clientupdate/distsign"
2023-02-24 22:15:35 +00:00
"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.
2023-08-24 21:36:47 +00:00
func CLI ( getTargets func ( ) ( [ ] dist . Target , error ) ) * ffcli . Command {
2023-02-24 22:15:35 +00:00
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 {
2023-08-24 21:36:47 +00:00
targets , err := getTargets ( )
2023-02-24 22:15:35 +00:00
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 {
2023-08-24 21:36:47 +00:00
targets , err := getTargets ( )
2023-02-24 22:15:35 +00:00
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" )
2023-03-02 01:05:31 +00:00
fs . BoolVar ( & buildArgs . verbose , "verbose" , false , "verbose logging" )
2023-08-21 23:37:54 +00:00
fs . StringVar ( & buildArgs . webClientRoot , "web-client-root" , "" , "path to root of web client source to build" )
2023-02-24 22:15:35 +00:00
return fs
} ) ( ) ,
LongHelp : strings . TrimSpace ( `
If filters are provided , only targets matching at least one filter are built .
Filters can use glob patterns ( * and ? ) .
` ) ,
} ,
2023-08-22 23:29:56 +00:00
{
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 )
2023-08-23 23:13:03 +00:00
fs . BoolVar ( & genKeyArgs . root , "root" , false , "generate a root key" )
fs . BoolVar ( & genKeyArgs . signing , "signing" , false , "generate a signing key" )
2023-08-22 23:29:56 +00:00
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
} ) ( ) ,
} ,
2023-08-24 17:54:42 +00:00
{
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
} ) ( ) ,
} ,
2023-02-24 22:15:35 +00:00
} ,
Exec : func ( context . Context , [ ] string ) error { return flag . ErrHelp } ,
}
}
func runList ( ctx context . Context , filters [ ] string , targets [ ] dist . Target ) error {
2023-02-24 23:03:09 +00:00
if len ( filters ) == 0 {
filters = [ ] string { "all" }
}
2023-02-24 22:15:35 +00:00
tgts , err := dist . FilterTargets ( targets , filters )
if err != nil {
return err
}
for _ , tgt := range tgts {
fmt . Println ( tgt )
}
return nil
}
var buildArgs struct {
2023-07-31 22:47:00 +00:00
manifest string
verbose bool
2023-08-21 23:37:54 +00:00
webClientRoot string
2023-02-24 22:15:35 +00:00
}
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 )
}
b , err := dist . NewBuild ( wd , filepath . Join ( wd , "dist" ) )
if err != nil {
return fmt . Errorf ( "creating build context: %w" , err )
}
defer b . Close ( )
2023-03-02 01:05:31 +00:00
b . Verbose = buildArgs . verbose
2023-08-21 23:37:54 +00:00
b . WebClientSource = buildArgs . webClientRoot
2023-02-24 22:15:35 +00:00
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 {
2023-05-26 02:26:11 +00:00
if ! filepath . IsAbs ( out [ i ] ) {
out [ i ] = filepath . Join ( b . Out , out [ i ] )
}
rel , err := filepath . Rel ( filepath . Dir ( manifest ) , out [ i ] )
2023-02-24 22:15:35 +00:00
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
}
2023-07-31 22:47:00 +00:00
func parseSigningKey ( path string ) ( crypto . Signer , error ) {
if path == "" {
return nil , nil
}
raw , err := os . ReadFile ( path )
if err != nil {
return nil , err
}
b , rest := pem . Decode ( raw )
if b == nil {
return nil , fmt . Errorf ( "failed to decode PEM data in %q" , path )
}
if len ( rest ) > 0 {
return nil , fmt . Errorf ( "trailing data in %q, please check that the key file was not corrupted" , path )
}
return x509 . ParseECPrivateKey ( b . Bytes )
}
2023-08-22 23:29:56 +00:00
var genKeyArgs struct {
2023-08-23 23:13:03 +00:00
root bool
signing bool
2023-08-22 23:29:56 +00:00
privPath string
pubPath string
}
func runGenKey ( ctx context . Context ) error {
2023-08-23 23:13:03 +00:00
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 ( )
}
2023-08-22 23:29:56 +00:00
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
}
2023-08-24 17:54:42 +00:00
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 {
rootPubBundle , err := os . ReadFile ( verifyKeySignatureArgs . rootPubPath )
if err != nil {
return err
}
rootPubs , err := distsign . ParseRootKeyBundle ( rootPubBundle )
if err != nil {
return fmt . Errorf ( "parsing %q: %w" , verifyKeySignatureArgs . rootPubPath , err )
}
signPubBundle , err := os . ReadFile ( verifyKeySignatureArgs . signPubPath )
if err != nil {
return err
}
sig , err := os . ReadFile ( verifyKeySignatureArgs . sigPath )
if err != nil {
return err
}
if ! distsign . VerifyAny ( rootPubs , signPubBundle , sig ) {
return errors . New ( "signature not valid" )
}
fmt . Println ( "signature ok" )
return nil
}