diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b9d2b6..4cfd407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This document outlines major changes between releases. - Add support for virtual hosted style addressing (#446, #449) - Support new param `frostfs.graceful_close_on_switch_timeout` (#475) - Support patch object method (#479) +- Add `sign` command to `frostfs-s3-authmate` (#467) ### Changed - Update go version to go1.19 (#470) diff --git a/cmd/s3-authmate/modules/root.go b/cmd/s3-authmate/modules/root.go index 5a258b9..90e339c 100644 --- a/cmd/s3-authmate/modules/root.go +++ b/cmd/s3-authmate/modules/root.go @@ -68,4 +68,7 @@ GoVersion: {{ runtimeVersion }} rootCmd.AddCommand(registerUserCmd) initRegisterUserCmd() + + rootCmd.AddCommand(signCmd) + initSignCmd() } diff --git a/cmd/s3-authmate/modules/sign.go b/cmd/s3-authmate/modules/sign.go new file mode 100644 index 0000000..002df44 --- /dev/null +++ b/cmd/s3-authmate/modules/sign.go @@ -0,0 +1,115 @@ +package modules + +import ( + "errors" + "fmt" + "os" + "strings" + "time" + + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var signCmd = &cobra.Command{ + Use: "sign", + Short: "Sign arbitrary data using AWS Signature Version 4", + Long: `Generate signature for provided data using AWS credentials. Credentials must be placed in ~/.aws/credentials. +You can provide profile to load using --profile flag or explicitly provide credentials and region using +--aws-access-key-id, --aws-secret-access-key, --region. +Note to override credentials you must provide both access key and secret key.`, + Example: `frostfs-s3-authmate sign --data some-data +frostfs-s3-authmate sign --data file://data.txt +frostfs-s3-authmate sign --data file://data.txt --profile my-profile --time 2024-09-27 +frostfs-s3-authmate sign --data some-data --region ru --service s3 --time 2024-09-27 --aws-access-key-id ETaA2CadPcA7bAkLsML2PbTudXY8uRt2PDjCCwkvRv9s0FDCxWDXYc1SA1vKv8KbyCNsLY2AmAjJ92Vz5rgvsFCy --aws-secret-access-key c2d65ef2980f03f4f495bdebedeeae760496697880d61d106bb9a4e5cd2e0607`, + RunE: runSignCmd, +} + +const ( + serviceFlag = "s3" + timeFlag = "time" + dataFlag = "data" +) + +func initSignCmd() { + signCmd.Flags().StringP(dataFlag, "d", "", "Data to sign. Can be provided as string or as a file ('file://path-to-file')") + signCmd.Flags().String(profileFlag, "", "AWS profile to load") + signCmd.Flags().String(serviceFlag, "s3", "AWS service name to form signature") + signCmd.Flags().String(timeFlag, "", "Signing time in '2006-01-02' format (default is current UTC time)") + signCmd.Flags().String(regionFlag, "", "AWS region to use in signature (default is taken from ~/.aws/config)") + signCmd.Flags().String(awsAccessKeyIDFlag, "", "AWS access key id to sign data (default is taken from ~/.aws/credentials)") + signCmd.Flags().String(awsSecretAccessKeyFlag, "", "AWS secret access key to sign data (default is taken from ~/.aws/credentials)") + + _ = signCmd.MarkFlagRequired(dataFlag) +} + +func runSignCmd(cmd *cobra.Command, _ []string) error { + var cfg aws.Config + + if region := viper.GetString(regionFlag); region != "" { + cfg.Region = ®ion + } + accessKeyID := viper.GetString(awsAccessKeyIDFlag) + secretAccessKey := viper.GetString(awsSecretAccessKeyFlag) + + if accessKeyID != "" && secretAccessKey != "" { + cfg.Credentials = credentials.NewStaticCredentialsFromCreds(credentials.Value{ + AccessKeyID: accessKeyID, + SecretAccessKey: secretAccessKey, + }) + } else if accessKeyID != "" || secretAccessKey != "" { + return wrapPreparationError(fmt.Errorf("both flags '%s' and '%s' must be provided", accessKeyIDFlag, awsSecretAccessKeyFlag)) + } + + sess, err := session.NewSessionWithOptions(session.Options{ + Config: cfg, + Profile: viper.GetString(profileFlag), + SharedConfigState: session.SharedConfigEnable, + }) + if err != nil { + return wrapPreparationError(fmt.Errorf("couldn't get aws credentials: %w", err)) + } + + data := viper.GetString(dataFlag) + if strings.HasPrefix(data, "file://") { + dataToSign, err := os.ReadFile(data[7:]) + if err != nil { + return wrapPreparationError(fmt.Errorf("read data file: %w", err)) + } + data = string(dataToSign) + } + + creds, err := sess.Config.Credentials.Get() + if err != nil { + return wrapPreparationError(fmt.Errorf("get creds: %w", err)) + } + + if sess.Config.Region == nil || *sess.Config.Region == "" { + return wrapPreparationError(errors.New("missing region")) + } + + service := viper.GetString(serviceFlag) + if service == "" { + return wrapPreparationError(errors.New("missing service")) + } + + signTime := viper.GetTime(timeFlag) + if signTime.IsZero() { + signTime = time.Now() + } + + signature := auth.SignStr(creds.SecretAccessKey, service, *sess.Config.Region, signTime, data) + + cmd.Println("service:", service) + cmd.Println("region:", *sess.Config.Region) + cmd.Println("time:", signTime.UTC().Format("20060102")) + cmd.Println("accessKeyId:", creds.AccessKeyID) + cmd.Printf("secretAccessKey: [****************%s]\n", creds.SecretAccessKey[max(0, len(creds.SecretAccessKey)-4):]) + cmd.Println("signature:", signature) + + return nil +}