go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/client/cmd/cloudkms/common.go (about)

     1  // Copyright 2017 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package main
    16  
    17  import (
    18  	"context"
    19  	"io"
    20  	"os"
    21  	"strings"
    22  
    23  	"golang.org/x/oauth2"
    24  	"google.golang.org/api/option"
    25  
    26  	"github.com/maruel/subcommands"
    27  
    28  	cloudkms "cloud.google.com/go/kms/apiv1"
    29  
    30  	"go.chromium.org/luci/auth"
    31  	"go.chromium.org/luci/auth/client/authcli"
    32  	"go.chromium.org/luci/common/cli"
    33  	"go.chromium.org/luci/common/errors"
    34  	"go.chromium.org/luci/common/logging"
    35  )
    36  
    37  type commonFlags struct {
    38  	subcommands.CommandRunBase
    39  	authFlags      authcli.Flags
    40  	parsedAuthOpts auth.Options
    41  	keyPath        string
    42  }
    43  
    44  func (c *commonFlags) Init(authOpts auth.Options) {
    45  	c.authFlags.Register(&c.Flags, authOpts)
    46  }
    47  
    48  func (c *commonFlags) Parse(args []string) error {
    49  	var err error
    50  	c.parsedAuthOpts, err = c.authFlags.Options()
    51  	if err != nil {
    52  		return err
    53  	}
    54  
    55  	if len(args) < 1 {
    56  		return errors.New("positional arguments missing")
    57  	}
    58  	if len(args) > 1 {
    59  		return errors.New("unexpected positional arguments")
    60  	}
    61  	if err := validateCryptoKeysKMSPath(args[0]); err != nil {
    62  		return err
    63  	}
    64  	c.keyPath = args[0]
    65  
    66  	return nil
    67  }
    68  
    69  func (c *commonFlags) createAuthTokenSource(ctx context.Context) (oauth2.TokenSource, error) {
    70  	a := auth.NewAuthenticator(ctx, auth.SilentLogin, c.parsedAuthOpts)
    71  	if err := a.CheckLoginRequired(); err != nil {
    72  		return nil, errors.Annotate(err, "please login with `luci-auth login`").Err()
    73  	}
    74  	return a.TokenSource()
    75  }
    76  
    77  func (c *commonFlags) commonMain(ctx context.Context) (*cloudkms.KeyManagementClient, error) {
    78  	// Set up service.
    79  	authTS, err := c.createAuthTokenSource(ctx)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  	client, err := cloudkms.NewKeyManagementClient(ctx, option.WithTokenSource(authTS))
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	return client, nil
    89  }
    90  
    91  func readInput(file string) ([]byte, error) {
    92  	if file == "-" {
    93  		return io.ReadAll(os.Stdin)
    94  	}
    95  	return os.ReadFile(file)
    96  }
    97  
    98  func readInputFd(file string) (*os.File, error) {
    99  	if file == "-" {
   100  		return os.Stdin, nil
   101  	}
   102  	return os.Open(file)
   103  }
   104  
   105  func writeOutput(file string, data []byte) error {
   106  	if file == "-" {
   107  		_, err := os.Stdout.Write(data)
   108  		return err
   109  	}
   110  	return os.WriteFile(file, data, 0664)
   111  }
   112  
   113  // cryptoKeysPathComponents are the path components necessary for API calls related to
   114  // crypto keys.
   115  //
   116  // This structure represents the following path format:
   117  // projects/.../locations/.../keyRings/.../cryptoKeys/...
   118  var cryptoKeysPathComponents = []string{
   119  	"projects",
   120  	"locations",
   121  	"keyRings",
   122  	"cryptoKeys",
   123  	"cryptoKeyVersions",
   124  }
   125  
   126  // validateCryptoKeysKMSPath validates a cloudkms path used for the API calls currently
   127  // supported by this client.
   128  //
   129  // What this means is we only care about paths that look exactly like the ones
   130  // constructed from kmsPathComponents.
   131  func validateCryptoKeysKMSPath(path string) error {
   132  	if path[0] == '/' {
   133  		path = path[1:]
   134  	}
   135  	components := strings.Split(path, "/")
   136  	if len(components) < (len(cryptoKeysPathComponents)-1)*2 || len(components) > len(cryptoKeysPathComponents)*2 {
   137  		return errors.Reason("path should have the form %s", strings.Join(cryptoKeysPathComponents, "/.../")+"/...").Err()
   138  	}
   139  	for i, c := range components {
   140  		if i%2 == 1 {
   141  			continue
   142  		}
   143  		expect := cryptoKeysPathComponents[i/2]
   144  		if c != expect {
   145  			return errors.Reason("expected component %d to be %s, got %s", i+1, expect, c).Err()
   146  		}
   147  	}
   148  	return nil
   149  }
   150  
   151  type verifyRun struct {
   152  	commonFlags
   153  	input    string
   154  	inputSig string
   155  	doVerify func(ctx context.Context, client *cloudkms.KeyManagementClient, input *os.File, inputSig []byte, keyPath string) error
   156  }
   157  
   158  func (v *verifyRun) Init(authOpts auth.Options) {
   159  	v.commonFlags.Init(authOpts)
   160  	v.Flags.StringVar(&v.input, "input", "", "Path to file with data to verify (use '-' for stdin).")
   161  	v.Flags.StringVar(&v.inputSig, "input-sig", "", "Path to read signature from (use '-' for stdin).")
   162  }
   163  
   164  func (v *verifyRun) Parse(ctx context.Context, args []string) error {
   165  	if err := v.commonFlags.Parse(args); err != nil {
   166  		return err
   167  	}
   168  	if v.input == "" {
   169  		return errors.New("input file is required")
   170  	}
   171  	if v.inputSig == "" {
   172  		return errors.New("input signature is required")
   173  	}
   174  	return nil
   175  }
   176  
   177  func (v *verifyRun) main(ctx context.Context) error {
   178  	service, err := v.commonMain(ctx)
   179  	if err != nil {
   180  		return err
   181  	}
   182  
   183  	// Open input file descriptor.
   184  	fd, err := readInputFd(v.input)
   185  	if err != nil {
   186  		return err
   187  	}
   188  	defer fd.Close()
   189  
   190  	// Read in signature.
   191  	sigBytes, err := readInput(v.inputSig)
   192  	if err != nil {
   193  		return err
   194  	}
   195  
   196  	return v.doVerify(ctx, service, fd, sigBytes, v.keyPath)
   197  }
   198  
   199  func (v *verifyRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
   200  	ctx := cli.GetContext(a, v, env)
   201  	if err := v.Parse(ctx, args); err != nil {
   202  		logging.WithError(err).Errorf(ctx, "Error while parsing arguments")
   203  		return 1
   204  	}
   205  	if err := v.main(ctx); err != nil {
   206  		logging.WithError(err).Errorf(ctx, "Error while executing command")
   207  		return 1
   208  	}
   209  	return 0
   210  }
   211  
   212  type signRun struct {
   213  	commonFlags
   214  	input  string
   215  	output string
   216  	doSign func(ctx context.Context, client *cloudkms.KeyManagementClient, input *os.File, keyPath string) ([]byte, error)
   217  }
   218  
   219  func (s *signRun) Init(authOpts auth.Options) {
   220  	s.commonFlags.Init(authOpts)
   221  	s.Flags.StringVar(&s.input, "input", "", "Path to file with data to sign (use '-' for stdin).")
   222  	s.Flags.StringVar(&s.output, "output", "", "Path to write signature to (use '-' for stdout).")
   223  }
   224  
   225  func (s *signRun) Parse(ctx context.Context, args []string) error {
   226  	if err := s.commonFlags.Parse(args); err != nil {
   227  		return err
   228  	}
   229  	if s.input == "" {
   230  		return errors.New("input file is required")
   231  	}
   232  	if s.output == "" {
   233  		return errors.New("output location is required")
   234  	}
   235  	return nil
   236  }
   237  
   238  func (s *signRun) main(ctx context.Context) error {
   239  	service, err := s.commonMain(ctx)
   240  	if err != nil {
   241  		return err
   242  	}
   243  
   244  	// Read in input.
   245  	fd, err := readInputFd(s.input)
   246  	if err != nil {
   247  		return err
   248  	}
   249  	defer fd.Close()
   250  
   251  	result, err := s.doSign(ctx, service, fd, s.keyPath)
   252  	if err != nil {
   253  		return err
   254  	}
   255  
   256  	// Write output.
   257  	return writeOutput(s.output, result)
   258  }
   259  
   260  func (s *signRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
   261  	ctx := cli.GetContext(a, s, env)
   262  	if err := s.Parse(ctx, args); err != nil {
   263  		logging.WithError(err).Errorf(ctx, "Error while parsing arguments")
   264  		return 1
   265  	}
   266  	if err := s.main(ctx); err != nil {
   267  		logging.WithError(err).Errorf(ctx, "Error while executing command")
   268  		return 1
   269  	}
   270  	return 0
   271  }
   272  
   273  type cryptRun struct {
   274  	commonFlags
   275  	input   string
   276  	output  string
   277  	doCrypt func(ctx context.Context, client *cloudkms.KeyManagementClient, input []byte, keyPath string) ([]byte, error)
   278  }
   279  
   280  func (c *cryptRun) Init(authOpts auth.Options) {
   281  	c.commonFlags.Init(authOpts)
   282  	c.Flags.StringVar(&c.input, "input", "", "Path to file with data to operate on (use '-' for stdin). Data for encrypt and decrypt cannot be larger than 64KiB.")
   283  	c.Flags.StringVar(&c.output, "output", "", "Path to write operation results to (use '-' for stdout).")
   284  }
   285  
   286  func (c *cryptRun) Parse(ctx context.Context, args []string) error {
   287  	if err := c.commonFlags.Parse(args); err != nil {
   288  		return err
   289  	}
   290  	if c.input == "" {
   291  		return errors.New("input file is required")
   292  	}
   293  	if c.output == "" {
   294  		return errors.New("output location is required")
   295  	}
   296  	return nil
   297  }
   298  
   299  func (c *cryptRun) main(ctx context.Context) error {
   300  	service, err := c.commonMain(ctx)
   301  	if err != nil {
   302  		return err
   303  	}
   304  
   305  	// Read in input.
   306  	bytes, err := readInput(c.input)
   307  	if err != nil {
   308  		return err
   309  	}
   310  
   311  	result, err := c.doCrypt(ctx, service, bytes, c.keyPath)
   312  	if err != nil {
   313  		return err
   314  	}
   315  
   316  	// Write output.
   317  	return writeOutput(c.output, result)
   318  }
   319  
   320  func (c *cryptRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
   321  	ctx := cli.GetContext(a, c, env)
   322  	if err := c.Parse(ctx, args); err != nil {
   323  		logging.WithError(err).Errorf(ctx, "Error while parsing arguments")
   324  		return 1
   325  	}
   326  	if err := c.main(ctx); err != nil {
   327  		logging.WithError(err).Errorf(ctx, "Error while executing command")
   328  		return 1
   329  	}
   330  	return 0
   331  }
   332  
   333  type downloadRun struct {
   334  	commonFlags
   335  	output     string
   336  	doDownload func(ctx context.Context, client *cloudkms.KeyManagementClient, keyPath string) ([]byte, error)
   337  }
   338  
   339  func (d *downloadRun) Init(authOpts auth.Options) {
   340  	d.commonFlags.Init(authOpts)
   341  	d.Flags.StringVar(&d.output, "output", "", "Path to write key to (use '-' for stdout).")
   342  }
   343  
   344  func (d *downloadRun) Parse(ctx context.Context, args []string) error {
   345  	if err := d.commonFlags.Parse(args); err != nil {
   346  		return err
   347  	}
   348  	if d.output == "" {
   349  		return errors.New("output location is required")
   350  	}
   351  	return nil
   352  }
   353  
   354  func (d *downloadRun) main(ctx context.Context) error {
   355  	service, err := d.commonMain(ctx)
   356  	if err != nil {
   357  		return err
   358  	}
   359  
   360  	result, err := d.doDownload(ctx, service, d.keyPath)
   361  	if err != nil {
   362  		return err
   363  	}
   364  
   365  	// Write output.
   366  	return writeOutput(d.output, result)
   367  }
   368  
   369  func (d *downloadRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
   370  	ctx := cli.GetContext(a, d, env)
   371  	if err := d.Parse(ctx, args); err != nil {
   372  		logging.WithError(err).Errorf(ctx, "Error while parsing arguments")
   373  		return 1
   374  	}
   375  	if err := d.main(ctx); err != nil {
   376  		logging.WithError(err).Errorf(ctx, "Error while executing command")
   377  		return 1
   378  	}
   379  	return 0
   380  }