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 }