From 2b9ca2a9f53fc120b82504e3b85646c34d8890b8 Mon Sep 17 00:00:00 2001
From: Denis Kirillov <d.kirillov@yadro.com>
Date: Fri, 27 Sep 2024 16:15:28 +0300
Subject: [PATCH] [#467] authmate: Add sign command

Support singing arbitrary data using aws sigv4 algorithm

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
---
 CHANGELOG.md                    |   1 +
 cmd/s3-authmate/modules/root.go |   3 +
 cmd/s3-authmate/modules/sign.go | 115 ++++++++++++++++++++++++++++++++
 3 files changed, 119 insertions(+)
 create mode 100644 cmd/s3-authmate/modules/sign.go

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b9d2b6e..4cfd4075 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 5a258b9d..90e339c4 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 00000000..002df44e
--- /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 = &region
+	}
+	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
+}
-- 
2.45.3