[#339] sigv4a: Support presign

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
Denis Kirillov 2024-04-23 14:49:34 +03:00
parent a692cb87fb
commit f09229c76e
8 changed files with 64 additions and 30 deletions

View file

@ -9,7 +9,6 @@ import (
"io" "io"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"os"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@ -23,7 +22,6 @@ import (
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials" credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/smithy-go/logging"
) )
var ( var (
@ -420,8 +418,6 @@ func (c *Center) checkSign(authHeader *AuthHeader, box *accessbox.Box, request *
case signaturePreambleSigV4A: case signaturePreambleSigV4A:
signer := v4a.NewSigner(func(options *v4a.SignerOptions) { signer := v4a.NewSigner(func(options *v4a.SignerOptions) {
options.DisableURIPathEscaping = true options.DisableURIPathEscaping = true
options.LogSigning = true
options.Logger = logging.NewStandardLogger(os.Stdout)
}) })
credAdapter := v4a.SymmetricCredentialAdaptor{ credAdapter := v4a.SymmetricCredentialAdaptor{
@ -433,13 +429,16 @@ func (c *Center) checkSign(authHeader *AuthHeader, box *accessbox.Box, request *
return fmt.Errorf("failed to derive assymetric key from credentials: %w", err) return fmt.Errorf("failed to derive assymetric key from credentials: %w", err)
} }
if authHeader.IsPresigned { if !authHeader.IsPresigned {
if err = checkPresignedDate(authHeader, signatureDateTime); err != nil { return signer.VerifySignature(creds, request, authHeader.PayloadHash, authHeader.Service,
return err strings.Split(authHeader.Region, ","), signatureDateTime, authHeader.Signature)
}
} }
return signer.VerifySignature(creds, request, authHeader.PayloadHash, authHeader.Service, if err = checkPresignedDate(authHeader, signatureDateTime); err != nil {
return err
}
return signer.VerifyPresigned(creds, request, authHeader.PayloadHash, authHeader.Service,
strings.Split(authHeader.Region, ","), signatureDateTime, authHeader.Signature) strings.Split(authHeader.Region, ","), signatureDateTime, authHeader.Signature)
default: default:
return fmt.Errorf("invalid preamble: %s", authHeader.Preamble) return fmt.Errorf("invalid preamble: %s", authHeader.Preamble)

View file

@ -13,8 +13,8 @@ import (
"time" "time"
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4" v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache"
v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2" v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
@ -23,10 +23,9 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test" oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest" "go.uber.org/zap/zaptest"
) )
@ -53,6 +52,7 @@ func TestAuthHeaderParse(t *testing.T) {
Signature: "2811ccb9e242f41426738fb1f", Signature: "2811ccb9e242f41426738fb1f",
SignedFields: []string{"host", "x-amz-content-sha256", "x-amz-date"}, SignedFields: []string{"host", "x-amz-content-sha256", "x-amz-date"},
Date: "20210809", Date: "20210809",
Preamble: signaturePreambleSigV4,
}, },
}, },
{ {

View file

@ -28,6 +28,7 @@ type PresignData struct {
Region string Region string
Lifetime time.Duration Lifetime time.Duration
SignTime time.Time SignTime time.Time
Headers map[string]string
} }
// PresignRequest forms pre-signed request to access objects without aws credentials. // PresignRequest forms pre-signed request to access objects without aws credentials.
@ -38,8 +39,10 @@ func PresignRequest(creds *credentials.Credentials, reqData RequestData, presign
return nil, fmt.Errorf("failed to create new request: %w", err) return nil, fmt.Errorf("failed to create new request: %w", err)
} }
for k, v := range presignData.Headers {
req.Header.Set(k, v) // maybe we should filter system header (or keep responsibility on caller)
}
req.Header.Set(AmzDate, presignData.SignTime.Format("20060102T150405Z")) req.Header.Set(AmzDate, presignData.SignTime.Format("20060102T150405Z"))
req.Header.Set(ContentTypeHdr, "text/plain")
signer := v4.NewSigner(creds) signer := v4.NewSigner(creds)
signer.DisableURIPathEscaping = true signer.DisableURIPathEscaping = true
@ -59,8 +62,11 @@ func PresignRequestV4a(credProvider credentialsv2.StaticCredentialsProvider, req
return nil, fmt.Errorf("failed to create new request: %w", err) return nil, fmt.Errorf("failed to create new request: %w", err)
} }
for k, v := range presignData.Headers {
req.Header.Set(k, v) // maybe we should filter system header (or keep responsibility on caller)
}
req.Header.Set(AmzDate, presignData.SignTime.Format("20060102T150405Z")) req.Header.Set(AmzDate, presignData.SignTime.Format("20060102T150405Z"))
req.Header.Set(ContentTypeHdr, "text/plain")
req.Header.Set(AmzExpires, strconv.Itoa(int(presignData.Lifetime.Seconds()))) req.Header.Set(AmzExpires, strconv.Itoa(int(presignData.Lifetime.Seconds())))
signer := v4a.NewSigner(func(options *v4a.SignerOptions) { signer := v4a.NewSigner(func(options *v4a.SignerOptions) {

View file

@ -75,6 +75,9 @@ func TestCheckSign(t *testing.T) {
Region: "spb", Region: "spb",
Lifetime: 10 * time.Minute, Lifetime: 10 * time.Minute,
SignTime: time.Now().UTC(), SignTime: time.Now().UTC(),
Headers: map[string]string{
ContentTypeHdr: "text/plain",
},
} }
req, err := PresignRequest(awsCreds, reqData, presignData) req, err := PresignRequest(awsCreds, reqData, presignData)
@ -119,11 +122,16 @@ func TestCheckSignV4a(t *testing.T) {
Region: "spb", Region: "spb",
Lifetime: 10 * time.Minute, Lifetime: 10 * time.Minute,
SignTime: time.Now().UTC(), SignTime: time.Now().UTC(),
Headers: map[string]string{
ContentTypeHdr: "text/plain",
},
} }
req, err := PresignRequestV4a(awsCreds, reqData, presignData) req, err := PresignRequestV4a(awsCreds, reqData, presignData)
require.NoError(t, err) require.NoError(t, err)
req.Header.Set(ContentTypeHdr, "text/plain")
expBox := &accessbox.Box{ expBox := &accessbox.Box{
Gate: &accessbox.GateData{ Gate: &accessbox.GateData{
SecretKey: secretKey, SecretKey: secretKey,
@ -170,9 +178,7 @@ func TestPresignRequestV4a(t *testing.T) {
req, err := http.NewRequest("GET", "http://localhost:8084/bucket/object", nil) req, err := http.NewRequest("GET", "http://localhost:8084/bucket/object", nil)
require.NoError(t, err) require.NoError(t, err)
//req.Header.Set(AmzRegionSet, strings.Join(regionSet, ",")) req.Header.Set(AmzExpires, "600")
//req.Header.Set(AmzDate, signingTime.Format("20060102T150405Z"))
//req.Header.Set(AmzAlgorithm, signaturePreambleSigV4A)
presignedURL, hdr, err := signer.PresignHTTP(req.Context(), creds, req, "", service, regionSet, signingTime) presignedURL, hdr, err := signer.PresignHTTP(req.Context(), creds, req, "", service, regionSet, signingTime)
require.NoError(t, err) require.NoError(t, err)
@ -184,7 +190,10 @@ func TestPresignRequestV4a(t *testing.T) {
r, err := http.NewRequest("GET", presignedURL, nil) r, err := http.NewRequest("GET", presignedURL, nil)
require.NoError(t, err) require.NoError(t, err)
query := r.URL.Query()
query.Del(AmzSignature)
r.URL.RawQuery = query.Encode()
err = signer.VerifySignature(creds, r, "", service, regionSet, signingTime, signature) err = signer.VerifyPresigned(creds, r, "", service, regionSet, signingTime, signature)
require.NoError(t, err) require.NoError(t, err)
} }

View file

@ -202,6 +202,15 @@ func (s *Signer) SignHTTP(ctx context.Context, credentials Credentials, r *http.
// VerifySignature checks sigv4a. // VerifySignature checks sigv4a.
func (s *Signer) VerifySignature(credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, signature string, optFns ...func(*SignerOptions)) error { func (s *Signer) VerifySignature(credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, signature string, optFns ...func(*SignerOptions)) error {
return s.verifySignature(credentials, r, payloadHash, service, regionSet, signingTime, signature, false, optFns...)
}
// VerifyPresigned checks sigv4a.
func (s *Signer) VerifyPresigned(credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, signature string, optFns ...func(*SignerOptions)) error {
return s.verifySignature(credentials, r, payloadHash, service, regionSet, signingTime, signature, true, optFns...)
}
func (s *Signer) verifySignature(credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, signature string, isPresigned bool, optFns ...func(*SignerOptions)) error {
options := s.options options := s.options
for _, fn := range optFns { for _, fn := range optFns {
fn(&options) fn(&options)
@ -214,6 +223,7 @@ func (s *Signer) VerifySignature(credentials Credentials, r *http.Request, paylo
RegionSet: regionSet, RegionSet: regionSet,
Credentials: credentials, Credentials: credentials,
Time: signingTime.UTC(), Time: signingTime.UTC(),
IsPreSign: isPresigned,
DisableHeaderHoisting: options.DisableHeaderHoisting, DisableHeaderHoisting: options.DisableHeaderHoisting,
DisableURIPathEscaping: options.DisableURIPathEscaping, DisableURIPathEscaping: options.DisableURIPathEscaping,
} }
@ -465,11 +475,6 @@ func (s *httpSigner) buildCanonicalHeaders(host string, rule v4Internal.Rule, he
continue // ignored header continue // ignored header
} }
if strings.EqualFold(k, contentLengthHeader) {
// prevent signing already handled content-length header.
continue
}
lowerCaseKey := strings.ToLower(k) lowerCaseKey := strings.ToLower(k)
if _, ok := signed[lowerCaseKey]; ok { if _, ok := signed[lowerCaseKey]; ok {
// include additional values // include additional values

View file

@ -3,10 +3,12 @@ package modules
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http"
"os" "os"
"time" "time"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth"
credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
@ -36,6 +38,7 @@ const (
regionFlag = "region" regionFlag = "region"
awsAccessKeyIDFlag = "aws-access-key-id" awsAccessKeyIDFlag = "aws-access-key-id"
awsSecretAccessKeyFlag = "aws-secret-access-key" awsSecretAccessKeyFlag = "aws-secret-access-key"
sigV4AFlag = "sigv4a"
) )
func initGeneratePresignedURLCmd() { func initGeneratePresignedURLCmd() {
@ -48,6 +51,7 @@ func initGeneratePresignedURLCmd() {
generatePresignedURLCmd.Flags().String(regionFlag, "", "AWS region to use in signature (default is taken from ~/.aws/config)") generatePresignedURLCmd.Flags().String(regionFlag, "", "AWS region to use in signature (default is taken from ~/.aws/config)")
generatePresignedURLCmd.Flags().String(awsAccessKeyIDFlag, "", "AWS access key id to sign the URL (default is taken from ~/.aws/credentials)") generatePresignedURLCmd.Flags().String(awsAccessKeyIDFlag, "", "AWS access key id to sign the URL (default is taken from ~/.aws/credentials)")
generatePresignedURLCmd.Flags().String(awsSecretAccessKeyFlag, "", "AWS secret access key to sign the URL (default is taken from ~/.aws/credentials)") generatePresignedURLCmd.Flags().String(awsSecretAccessKeyFlag, "", "AWS secret access key to sign the URL (default is taken from ~/.aws/credentials)")
generatePresignedURLCmd.Flags().Bool(sigV4AFlag, false, "Use SigV4A for signing request")
_ = generatePresignedURLCmd.MarkFlagRequired(endpointFlag) _ = generatePresignedURLCmd.MarkFlagRequired(endpointFlag)
_ = generatePresignedURLCmd.MarkFlagRequired(bucketFlag) _ = generatePresignedURLCmd.MarkFlagRequired(bucketFlag)
@ -92,7 +96,18 @@ func runGeneratePresignedURLCmd(*cobra.Command, []string) error {
SignTime: time.Now().UTC(), SignTime: time.Now().UTC(),
} }
req, err := auth.PresignRequest(sess.Config.Credentials, reqData, presignData) var req *http.Request
if viper.GetBool(sigV4AFlag) {
val, err := sess.Config.Credentials.Get()
if err != nil {
return wrapPreparationError(err)
}
awsCreds := credentialsv2.NewStaticCredentialsProvider(val.AccessKeyID, val.SecretAccessKey, "")
req, err = auth.PresignRequestV4a(awsCreds, reqData, presignData)
} else {
req, err = auth.PresignRequest(sess.Config.Credentials, reqData, presignData)
}
if err != nil { if err != nil {
return wrapBusinessLogicError(err) return wrapBusinessLogicError(err)
} }

1
go.mod
View file

@ -47,7 +47,6 @@ require (
git.frostfs.info/TrueCloudLab/tzhash v1.8.0 // indirect git.frostfs.info/TrueCloudLab/tzhash v1.8.0 // indirect
github.com/VictoriaMetrics/easyproto v0.1.4 // indirect github.com/VictoriaMetrics/easyproto v0.1.4 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect

11
go.sum
View file

@ -66,10 +66,12 @@ github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/aws/aws-sdk-go v1.44.6 h1:Y+uHxmZfhRTLX2X3khkdxCoTZAyGEX21aOUHe1U6geg= github.com/aws/aws-sdk-go v1.44.6 h1:Y+uHxmZfhRTLX2X3khkdxCoTZAyGEX21aOUHe1U6geg=
github.com/aws/aws-sdk-go v1.44.6/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go v1.44.6/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go-v2 v1.18.1 h1:+tefE750oAb7ZQGzla6bLkOwfcQCEtC5y2RqoqCeqKo= github.com/aws/aws-sdk-go-v2 v1.26.0 h1:/Ce4OCiM3EkpW7Y+xUnfAFpchU78K7/Ug01sZni9PgA=
github.com/aws/aws-sdk-go-v2 v1.18.1/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= github.com/aws/aws-sdk-go-v2 v1.26.0/go.mod h1:35hUlJVYd+M++iLI3ALmVwMOyRYMmRqUXpTtRGW+K9I=
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= github.com/aws/aws-sdk-go-v2/credentials v1.17.9 h1:N8s0/7yW+h8qR8WaRlPQeJ6czVMNQVNtNdUqf6cItao=
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/aws/aws-sdk-go-v2/credentials v1.17.9/go.mod h1:446YhIdmSV0Jf/SLafGZalQo+xr2iw7/fzXGDPTU1yQ=
github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw=
github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.8.0 h1:FD+XqgOZDUxxZ8hzoBFuV9+cGWY9CslN6d5MS5JVb4c= github.com/bits-and-blooms/bitset v1.8.0 h1:FD+XqgOZDUxxZ8hzoBFuV9+cGWY9CslN6d5MS5JVb4c=
@ -177,7 +179,6 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=