From f3ac5d19343922c4168c2d104754dcc6038a60a6 Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Wed, 27 Mar 2024 09:14:58 +0300 Subject: [PATCH 1/8] [#339] Add aws-sdk-go-v2 Signed-off-by: Denis Kirillov --- api/auth/center.go | 188 ++++-- api/auth/center_test.go | 42 +- api/auth/presign.go | 41 ++ api/auth/presign_test.go | 96 +++ api/auth/signer/v4asdk2/credentials.go | 140 +++++ api/auth/signer/v4asdk2/credentials_test.go | 77 +++ api/auth/signer/v4asdk2/error.go | 17 + .../signer/v4asdk2/internal/crypto/compare.go | 30 + .../v4asdk2/internal/crypto/compare_test.go | 60 ++ .../signer/v4asdk2/internal/crypto/ecc.go | 113 ++++ .../v4asdk2/internal/crypto/ecc_test.go | 277 +++++++++ api/auth/signer/v4asdk2/internal/v4/const.go | 36 ++ .../v4asdk2/internal/v4/header_rules.go | 88 +++ .../signer/v4asdk2/internal/v4/headers.go | 79 +++ api/auth/signer/v4asdk2/internal/v4/hmac.go | 13 + api/auth/signer/v4asdk2/internal/v4/host.go | 75 +++ api/auth/signer/v4asdk2/internal/v4/time.go | 36 ++ api/auth/signer/v4asdk2/internal/v4/util.go | 64 ++ .../signer/v4asdk2/internal/v4/util_test.go | 75 +++ api/auth/signer/v4asdk2/middleware.go | 118 ++++ api/auth/signer/v4asdk2/middleware_test.go | 149 +++++ api/auth/signer/v4asdk2/presign_middleware.go | 116 ++++ .../signer/v4asdk2/presign_middleware_test.go | 222 +++++++ api/auth/signer/v4asdk2/shared_test.go | 18 + api/auth/signer/v4asdk2/smithy.go | 85 +++ api/auth/signer/v4asdk2/stream.go | 96 +++ api/auth/signer/v4asdk2/v4a.go | 577 ++++++++++++++++++ api/auth/signer/v4asdk2/v4a_test.go | 429 +++++++++++++ .../signer/v4sdk2/signer/internal/v4/cache.go | 115 ++++ .../signer/v4sdk2/signer/internal/v4/const.go | 40 ++ .../v4sdk2/signer/internal/v4/header_rules.go | 88 +++ .../v4sdk2/signer/internal/v4/headers.go | 84 +++ .../v4sdk2/signer/internal/v4/headers_test.go | 63 ++ .../signer/v4sdk2/signer/internal/v4/hmac.go | 13 + .../signer/v4sdk2/signer/internal/v4/host.go | 75 +++ .../signer/v4sdk2/signer/internal/v4/scope.go | 13 + .../signer/v4sdk2/signer/internal/v4/time.go | 36 ++ .../signer/v4sdk2/signer/internal/v4/util.go | 80 +++ .../v4sdk2/signer/internal/v4/util_test.go | 158 +++++ .../signer/v4sdk2/signer/v4/middleware.go | 442 ++++++++++++++ .../v4sdk2/signer/v4/presign_middleware.go | 126 ++++ api/auth/signer/v4sdk2/signer/v4/stream.go | 87 +++ api/auth/signer/v4sdk2/signer/v4/v4.go | 570 +++++++++++++++++ api/auth/signer/v4sdk2/signer/v4/v4_test.go | 363 +++++++++++ api/handler/put.go | 14 +- api/handler/s3v4aReader.go | 209 +++++++ api/headers.go | 11 +- go.mod | 1 + 48 files changed, 5894 insertions(+), 51 deletions(-) create mode 100644 api/auth/signer/v4asdk2/credentials.go create mode 100644 api/auth/signer/v4asdk2/credentials_test.go create mode 100644 api/auth/signer/v4asdk2/error.go create mode 100644 api/auth/signer/v4asdk2/internal/crypto/compare.go create mode 100644 api/auth/signer/v4asdk2/internal/crypto/compare_test.go create mode 100644 api/auth/signer/v4asdk2/internal/crypto/ecc.go create mode 100644 api/auth/signer/v4asdk2/internal/crypto/ecc_test.go create mode 100644 api/auth/signer/v4asdk2/internal/v4/const.go create mode 100644 api/auth/signer/v4asdk2/internal/v4/header_rules.go create mode 100644 api/auth/signer/v4asdk2/internal/v4/headers.go create mode 100644 api/auth/signer/v4asdk2/internal/v4/hmac.go create mode 100644 api/auth/signer/v4asdk2/internal/v4/host.go create mode 100644 api/auth/signer/v4asdk2/internal/v4/time.go create mode 100644 api/auth/signer/v4asdk2/internal/v4/util.go create mode 100644 api/auth/signer/v4asdk2/internal/v4/util_test.go create mode 100644 api/auth/signer/v4asdk2/middleware.go create mode 100644 api/auth/signer/v4asdk2/middleware_test.go create mode 100644 api/auth/signer/v4asdk2/presign_middleware.go create mode 100644 api/auth/signer/v4asdk2/presign_middleware_test.go create mode 100644 api/auth/signer/v4asdk2/shared_test.go create mode 100644 api/auth/signer/v4asdk2/smithy.go create mode 100644 api/auth/signer/v4asdk2/stream.go create mode 100644 api/auth/signer/v4asdk2/v4a.go create mode 100644 api/auth/signer/v4asdk2/v4a_test.go create mode 100644 api/auth/signer/v4sdk2/signer/internal/v4/cache.go create mode 100644 api/auth/signer/v4sdk2/signer/internal/v4/const.go create mode 100644 api/auth/signer/v4sdk2/signer/internal/v4/header_rules.go create mode 100644 api/auth/signer/v4sdk2/signer/internal/v4/headers.go create mode 100644 api/auth/signer/v4sdk2/signer/internal/v4/headers_test.go create mode 100644 api/auth/signer/v4sdk2/signer/internal/v4/hmac.go create mode 100644 api/auth/signer/v4sdk2/signer/internal/v4/host.go create mode 100644 api/auth/signer/v4sdk2/signer/internal/v4/scope.go create mode 100644 api/auth/signer/v4sdk2/signer/internal/v4/time.go create mode 100644 api/auth/signer/v4sdk2/signer/internal/v4/util.go create mode 100644 api/auth/signer/v4sdk2/signer/internal/v4/util_test.go create mode 100644 api/auth/signer/v4sdk2/signer/v4/middleware.go create mode 100644 api/auth/signer/v4sdk2/signer/v4/presign_middleware.go create mode 100644 api/auth/signer/v4sdk2/signer/v4/stream.go create mode 100644 api/auth/signer/v4sdk2/signer/v4/v4.go create mode 100644 api/auth/signer/v4sdk2/signer/v4/v4_test.go create mode 100644 api/handler/s3v4aReader.go diff --git a/api/auth/center.go b/api/auth/center.go index e654c25..2f2b8e1 100644 --- a/api/auth/center.go +++ b/api/auth/center.go @@ -9,29 +9,39 @@ import ( "io" "mime/multipart" "net/http" + "os" "regexp" "strings" "time" v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4" apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" + v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" + credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/smithy-go/logging" ) -// AuthorizationFieldRegexp -- is regexp for credentials with Base58 encoded cid and oid and '0' (zero) as delimiter. -var AuthorizationFieldRegexp = regexp.MustCompile(`AWS4-HMAC-SHA256 Credential=(?P[^/]+)/(?P[^/]+)/(?P[^/]*)/(?P[^/]+)/aws4_request,\s*SignedHeaders=(?P.+),\s*Signature=(?P.+)`) +var ( + // AuthorizationFieldRegexp -- is regexp for credentials with Base58 encoded cid and oid and '0' (zero) as delimiter. + AuthorizationFieldRegexp = regexp.MustCompile(`AWS4-HMAC-SHA256 Credential=(?P[^/]+)/(?P[^/]+)/(?P[^/]*)/(?P[^/]+)/aws4_request,\s*SignedHeaders=(?P.+),\s*Signature=(?P.+)`) -// postPolicyCredentialRegexp -- is regexp for credentials when uploading file using POST with policy. -var postPolicyCredentialRegexp = regexp.MustCompile(`(?P[^/]+)/(?P[^/]+)/(?P[^/]*)/(?P[^/]+)/aws4_request`) + // authorizationFieldV4aRegexp -- is regexp for credentials with Base58 encoded cid and oid and '0' (zero) as delimiter. + authorizationFieldV4aRegexp = regexp.MustCompile(`AWS4-ECDSA-P256-SHA256 Credential=(?P[^/]+)/(?P[^/]+)/(?P[^/]+)/aws4_request,\s*SignedHeaders=(?P.+),\s*Signature=(?P.+)`) + + // postPolicyCredentialRegexp -- is regexp for credentials when uploading file using POST with policy. + postPolicyCredentialRegexp = regexp.MustCompile(`(?P[^/]+)/(?P[^/]+)/(?P[^/]*)/(?P[^/]+)/aws4_request`) +) type ( Center struct { reg *RegexpSubmatcher + regV4a *RegexpSubmatcher postReg *RegexpSubmatcher cli tokens.Credentials allowedAccessKeyIDPrefixes []string // empty slice means all access key ids are allowed @@ -47,22 +57,26 @@ type ( AccessKeyID string Service string Region string - SignatureV4 string + Signature string SignedFields []string Date string IsPresigned bool Expiration time.Duration + Preamble string + PayloadHash string } ) const ( - authHeaderPartsNum = 6 - maxFormSizeMemory = 50 * 1048576 // 50 MB + authHeaderPartsNum = 6 + authHeaderV4aPartsNum = 5 + maxFormSizeMemory = 50 * 1048576 // 50 MB AmzAlgorithm = "X-Amz-Algorithm" AmzCredential = "X-Amz-Credential" AmzSignature = "X-Amz-Signature" AmzSignedHeaders = "X-Amz-SignedHeaders" + AmzRegionSet = "X-Amz-Region-Set" AmzExpires = "X-Amz-Expires" AmzDate = "X-Amz-Date" AmzContentSHA256 = "X-Amz-Content-Sha256" @@ -91,27 +105,56 @@ func New(creds tokens.Credentials, prefixes []string, settings CenterSettings) * return &Center{ cli: creds, reg: NewRegexpMatcher(AuthorizationFieldRegexp), + regV4a: NewRegexpMatcher(authorizationFieldV4aRegexp), postReg: NewRegexpMatcher(postPolicyCredentialRegexp), allowedAccessKeyIDPrefixes: prefixes, settings: settings, } } -func (c *Center) parseAuthHeader(header string) (*AuthHeader, error) { - submatches := c.reg.GetSubmatches(header) - if len(submatches) != authHeaderPartsNum { - return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrAuthorizationHeaderMalformed), header) - } +const ( + signaturePreambleSigV4 = "AWS4-HMAC-SHA256" + signaturePreambleSigV4A = "AWS4-ECDSA-P256-SHA256" +) - signedFields := strings.Split(submatches["signed_header_fields"], ";") +func (c *Center) parseAuthHeader(authHeader string, headers http.Header) (*AuthHeader, error) { + preamble, _, _ := strings.Cut(authHeader, " ") + + var ( + submatches map[string]string + region string + ) + + switch preamble { + case signaturePreambleSigV4: + submatches = c.reg.GetSubmatches(authHeader) + if len(submatches) != authHeaderPartsNum { + return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrAuthorizationHeaderMalformed), authHeader) + } + region = submatches["region"] + case signaturePreambleSigV4A: + submatches = c.regV4a.GetSubmatches(authHeader) + if len(submatches) != authHeaderV4aPartsNum { + return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrAuthorizationHeaderMalformed), authHeader) + } + region = headers.Get(AmzRegionSet) + default: + return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrAuthorizationHeaderMalformed), authHeader) + } + // AWS4-ECDSA-P256-SHA256 + // Credential=2XGRML5EW3LMHdf64W2DkBy1Nkuu4y4wGhUj44QjbXBi05ZNvs8WVwy1XTmSEkcVkydPKzCgtmR7U3zyLYTj3Snxf/20240326/s3/aws4_request, + // SignedHeaders=amz-sdk-invocation-id;amz-sdk-request;host;x-amz-content-sha256;x-amz-date;x-amz-region-set, + // Signature=3044022006a2bc760140834101d0a79667d6aa75768c1a28e9cafc8963484d0752a6c6050220629dc06d7d6505e1b1e2a5d1f974b25ba32fdffc6f3f70dc4dda31b8a6f7ea2b return &AuthHeader{ AccessKeyID: submatches["access_key_id"], Service: submatches["service"], - Region: submatches["region"], - SignatureV4: submatches["v4_signature"], - SignedFields: signedFields, + Region: region, + Signature: submatches["v4_signature"], + SignedFields: strings.Split(submatches["signed_header_fields"], ";"), Date: submatches["date"], + Preamble: preamble, + PayloadHash: headers.Get(AmzContentSHA256), }, nil } @@ -129,7 +172,7 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) { ) queryValues := r.URL.Query() - if queryValues.Get(AmzAlgorithm) == "AWS4-HMAC-SHA256" { + if queryValues.Get(AmzAlgorithm) == signaturePreambleSigV4 { creds := strings.Split(queryValues.Get(AmzCredential), "/") if len(creds) != 5 || creds[4] != "aws4_request" { return nil, fmt.Errorf("bad X-Amz-Credential") @@ -138,10 +181,31 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) { AccessKeyID: creds[0], Service: creds[3], Region: creds[2], - SignatureV4: queryValues.Get(AmzSignature), + Signature: queryValues.Get(AmzSignature), SignedFields: strings.Split(queryValues.Get(AmzSignedHeaders), ";"), Date: creds[1], IsPresigned: true, + Preamble: signaturePreambleSigV4, + } + authHdr.Expiration, err = time.ParseDuration(queryValues.Get(AmzExpires) + "s") + if err != nil { + return nil, fmt.Errorf("couldn't parse X-Amz-Expires: %w", err) + } + signatureDateTimeStr = queryValues.Get(AmzDate) + } else if queryValues.Get(AmzAlgorithm) == signaturePreambleSigV4A { + creds := strings.Split(queryValues.Get(AmzCredential), "/") + if len(creds) != 4 || creds[3] != "aws4_request" { + return nil, fmt.Errorf("bad X-Amz-Credential") + } + authHdr = &AuthHeader{ + AccessKeyID: creds[0], + Service: creds[2], + Region: queryValues.Get(AmzRegionSet), + Signature: queryValues.Get(AmzSignature), + SignedFields: strings.Split(queryValues.Get(AmzSignedHeaders), ";"), + Date: creds[1], + IsPresigned: true, + Preamble: signaturePreambleSigV4A, } authHdr.Expiration, err = time.ParseDuration(queryValues.Get(AmzExpires) + "s") if err != nil { @@ -156,7 +220,7 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) { } return nil, fmt.Errorf("%w: %v", middleware.ErrNoAuthorizationHeader, authHeaderField) } - authHdr, err = c.parseAuthHeader(authHeaderField[0]) + authHdr, err = c.parseAuthHeader(authHeaderField[0], r.Header) if err != nil { return nil, err } @@ -197,7 +261,7 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) { AuthHeaders: &middleware.AuthHeader{ AccessKeyID: authHdr.AccessKeyID, Region: authHdr.Region, - SignatureV4: authHdr.SignatureV4, + SignatureV4: authHdr.Signature, }, Attributes: attrs, } @@ -331,37 +395,75 @@ func cloneRequest(r *http.Request, authHeader *AuthHeader) *http.Request { } func (c *Center) checkSign(authHeader *AuthHeader, box *accessbox.Box, request *http.Request, signatureDateTime time.Time) error { - awsCreds := credentials.NewStaticCredentials(authHeader.AccessKeyID, box.Gate.SecretKey, "") - signer := v4.NewSigner(awsCreds) - signer.DisableURIPathEscaping = true - var signature string - if authHeader.IsPresigned { - now := time.Now() - if signatureDateTime.Add(authHeader.Expiration).Before(now) { - return fmt.Errorf("%w: expired: now %s, signature %s", apierr.GetAPIError(apierr.ErrExpiredPresignRequest), - now.Format(time.RFC3339), signatureDateTime.Format(time.RFC3339)) + + switch authHeader.Preamble { + case signaturePreambleSigV4: + awsCreds := credentials.NewStaticCredentials(authHeader.AccessKeyID, box.Gate.SecretKey, "") + signer := v4.NewSigner(awsCreds) + signer.DisableURIPathEscaping = true + + if authHeader.IsPresigned { + if err := checkPresignedDate(authHeader, signatureDateTime); err != nil { + return err + } + + if _, err := signer.Presign(request, nil, authHeader.Service, authHeader.Region, authHeader.Expiration, signatureDateTime); err != nil { + return fmt.Errorf("failed to pre-sign temporary HTTP request: %w", err) + } + signature = request.URL.Query().Get(AmzSignature) + } else { + if _, err := signer.Sign(request, nil, authHeader.Service, authHeader.Region, signatureDateTime); err != nil { + return fmt.Errorf("failed to sign temporary HTTP request: %w", err) + } + signature = c.reg.GetSubmatches(request.Header.Get(AuthorizationHdr))["v4_signature"] } - if now.Before(signatureDateTime) { - return fmt.Errorf("%w: signature time from the future: now %s, signature %s", apierr.GetAPIError(apierr.ErrBadRequest), - now.Format(time.RFC3339), signatureDateTime.Format(time.RFC3339)) + if authHeader.Signature != signature { + return fmt.Errorf("%w: %s != %s: headers %v", apierr.GetAPIError(apierr.ErrSignatureDoesNotMatch), + authHeader.Signature, signature, authHeader.SignedFields) } - if _, err := signer.Presign(request, nil, authHeader.Service, authHeader.Region, authHeader.Expiration, signatureDateTime); err != nil { - return fmt.Errorf("failed to pre-sign temporary HTTP request: %w", err) + + case signaturePreambleSigV4A: + signer := v4a.NewSigner(func(options *v4a.SignerOptions) { + options.DisableURIPathEscaping = true + options.LogSigning = true + options.Logger = logging.NewStandardLogger(os.Stdout) + }) + + credAdapter := v4a.SymmetricCredentialAdaptor{ + SymmetricProvider: credentialsv2.NewStaticCredentialsProvider(authHeader.AccessKeyID, box.Gate.SecretKey, ""), } - signature = request.URL.Query().Get(AmzSignature) - } else { - if _, err := signer.Sign(request, nil, authHeader.Service, authHeader.Region, signatureDateTime); err != nil { - return fmt.Errorf("failed to sign temporary HTTP request: %w", err) + + creds, err := credAdapter.RetrievePrivateKey(request.Context()) + if err != nil { + return fmt.Errorf("failed to derive assymetric key from credentials: %w", err) } - signature = c.reg.GetSubmatches(request.Header.Get(AuthorizationHdr))["v4_signature"] + + if authHeader.IsPresigned { + if err = checkPresignedDate(authHeader, signatureDateTime); err != nil { + return err + } + } + + return signer.VerifySignature(creds, request, authHeader.PayloadHash, authHeader.Service, + strings.Split(authHeader.Region, ","), signatureDateTime, authHeader.Signature) + default: + return fmt.Errorf("invalid preamble: %s", authHeader.Preamble) } - if authHeader.SignatureV4 != signature { - return fmt.Errorf("%w: %s != %s: headers %v", apierr.GetAPIError(apierr.ErrSignatureDoesNotMatch), - authHeader.SignatureV4, signature, authHeader.SignedFields) - } + return nil +} +func checkPresignedDate(authHeader *AuthHeader, signatureDateTime time.Time) error { + now := time.Now() + if signatureDateTime.Add(authHeader.Expiration).Before(now) { + return fmt.Errorf("%w: expired: now %s, signature %s", apierr.GetAPIError(apierr.ErrExpiredPresignRequest), + now.Format(time.RFC3339), signatureDateTime.Format(time.RFC3339)) + } + if now.Before(signatureDateTime) { + return fmt.Errorf("%w: signature time from the future: now %s, signature %s", apierr.GetAPIError(apierr.ErrBadRequest), + now.Format(time.RFC3339), signatureDateTime.Format(time.RFC3339)) + } return nil } diff --git a/api/auth/center_test.go b/api/auth/center_test.go index c8c8261..f4e51dd 100644 --- a/api/auth/center_test.go +++ b/api/auth/center_test.go @@ -14,6 +14,7 @@ import ( 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" "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/tokens" @@ -25,6 +26,8 @@ import ( oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test" "github.com/aws/aws-sdk-go/aws/credentials" "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" "go.uber.org/zap/zaptest" ) @@ -60,7 +63,7 @@ func TestAuthHeaderParse(t *testing.T) { AccessKeyID: "oid0cid", Service: "s3", Region: "us-east-1", - SignatureV4: "2811ccb9e242f41426738fb1f", + Signature: "2811ccb9e242f41426738fb1f", SignedFields: []string{"host", "x-amz-content-sha256", "x-amz-date"}, Date: "20210809", }, @@ -71,7 +74,7 @@ func TestAuthHeaderParse(t *testing.T) { expected: nil, }, } { - authHeader, err := center.parseAuthHeader(tc.header) + authHeader, err := center.parseAuthHeader(tc.header, nil) require.ErrorIs(t, err, tc.err, tc.header) require.Equal(t, tc.expected, authHeader, tc.header) } @@ -90,6 +93,41 @@ func TestSignature(t *testing.T) { require.Equal(t, "dfbe886241d9e369cf4b329ca0f15eb27306c97aa1022cc0bb5a914c4ef87634", signature) } +func TestSignatureV4A(t *testing.T) { + var accessKeyAddr oid.Address + err := accessKeyAddr.DecodeString("8N7CYBY74kxZXoyvA5UNdmovaXqFpwNfvEPsqaN81es2/3tDwq5tR8fByrJcyJwyiuYX7Dae8tyDT7pd8oaL1MBto") + require.NoError(t, err) + + accessKeyID := strings.ReplaceAll(accessKeyAddr.String(), "/", "0") + secretKey := "713d0a0b9efc7d22923e17b0402a6a89b4273bc711c8bacb2da1b643d0006aeb" + + signer := v4a.NewSigner(func(options *v4a.SignerOptions) { + options.DisableURIPathEscaping = true + }) + + credAdapter := v4a.SymmetricCredentialAdaptor{ + SymmetricProvider: credentialsv2.NewStaticCredentialsProvider(accessKeyID, secretKey, ""), + } + + req, err := http.NewRequest("GET", "http://localhost:8084/bucket/object", nil) + require.NoError(t, err) + + creds, err := credAdapter.RetrievePrivateKey(req.Context()) + require.NoError(t, err) + + signingTime := time.Now() + service := "s3" + regionSet := []string{"spb"} + + err = signer.SignHTTP(req.Context(), creds, req, "", service, regionSet, signingTime) + require.NoError(t, err) + + signature := NewRegexpMatcher(authorizationFieldV4aRegexp).GetSubmatches(req.Header.Get(AuthorizationHdr))["v4_signature"] + + err = signer.VerifySignature(creds, req, "", service, regionSet, signingTime, signature) + require.NoError(t, err) +} + func TestCheckFormatContentSHA256(t *testing.T) { defaultErr := errors.GetAPIError(errors.ErrContentSHA256Mismatch) diff --git a/api/auth/presign.go b/api/auth/presign.go index df66d50..6d20b30 100644 --- a/api/auth/presign.go +++ b/api/auth/presign.go @@ -3,12 +3,17 @@ package auth import ( "fmt" "net/http" + "os" + "strconv" "strings" "time" v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4" + v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2" + credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/private/protocol/rest" + "github.com/aws/smithy-go/logging" ) type RequestData struct { @@ -49,3 +54,39 @@ func PresignRequest(creds *credentials.Credentials, reqData RequestData, presign return req, nil } + +// PresignRequestV4a forms pre-signed request to access objects without aws credentials. +func PresignRequestV4a(credProvider credentialsv2.StaticCredentialsProvider, reqData RequestData, presignData PresignData) (*http.Request, error) { + urlStr := fmt.Sprintf("%s/%s/%s", reqData.Endpoint, rest.EscapePath(reqData.Bucket, false), rest.EscapePath(reqData.Object, false)) + req, err := http.NewRequest(strings.ToUpper(reqData.Method), urlStr, nil) + if err != nil { + return nil, fmt.Errorf("failed to create new request: %w", err) + } + + req.Header.Set(AmzDate, presignData.SignTime.Format("20060102T150405Z")) + req.Header.Set(ContentTypeHdr, "text/plain") + req.Header.Set(AmzExpires, strconv.Itoa(int(presignData.Lifetime.Seconds()))) + + signer := v4a.NewSigner(func(options *v4a.SignerOptions) { + options.DisableURIPathEscaping = true + options.LogSigning = true + options.Logger = logging.NewStandardLogger(os.Stdout) + }) + + credAdapter := v4a.SymmetricCredentialAdaptor{ + SymmetricProvider: credProvider, + } + + creds, err := credAdapter.RetrievePrivateKey(req.Context()) + if err != nil { + return nil, fmt.Errorf("failed to derive assymetric key from credentials: %w", err) + } + presignedURL, _, err := signer.PresignHTTP(req.Context(), creds, req, "", presignData.Service, []string{presignData.Region}, presignData.SignTime) + if err != nil { + return nil, fmt.Errorf("presign: %w", err) + } + + fmt.Println(presignedURL) + + return http.NewRequest(reqData.Method, presignedURL, nil) +} diff --git a/api/auth/presign_test.go b/api/auth/presign_test.go index f669b1b..27f8084 100644 --- a/api/auth/presign_test.go +++ b/api/auth/presign_test.go @@ -2,17 +2,23 @@ package auth import ( "context" + "fmt" + "net/http" + "os" "strings" "testing" "time" + v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens" apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" + credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/smithy-go/logging" "github.com/stretchr/testify/require" ) @@ -93,3 +99,93 @@ func TestCheckSign(t *testing.T) { require.NoError(t, err) require.EqualValues(t, expBox, box.AccessBox) } + +func TestCheckSignV4a(t *testing.T) { + var accessKeyAddr oid.Address + err := accessKeyAddr.DecodeString("8N7CYBY74kxZXoyvA5UNdmovaXqFpwNfvEPsqaN81es2/3tDwq5tR8fByrJcyJwyiuYX7Dae8tyDT7pd8oaL1MBto") + require.NoError(t, err) + + accessKeyID := strings.ReplaceAll(accessKeyAddr.String(), "/", "0") + secretKey := "713d0a0b9efc7d22923e17b0402a6a89b4273bc711c8bacb2da1b643d0006aeb" + awsCreds := credentialsv2.NewStaticCredentialsProvider(accessKeyID, secretKey, "") + + reqData := RequestData{ + Method: "GET", + Endpoint: "http://localhost:8084", + Bucket: "my-bucket", + Object: "@obj/name", + } + presignData := PresignData{ + Service: "s3", + Region: "spb", + Lifetime: 10 * time.Minute, + SignTime: time.Now().UTC(), + } + + req, err := PresignRequestV4a(awsCreds, reqData, presignData) + require.NoError(t, err) + + expBox := &accessbox.Box{ + Gate: &accessbox.GateData{ + SecretKey: secretKey, + }, + } + + mock := newTokensFrostfsMock() + mock.addBox(accessKeyAddr, expBox) + + c := &Center{ + cli: mock, + regV4a: NewRegexpMatcher(authorizationFieldV4aRegexp), + postReg: NewRegexpMatcher(postPolicyCredentialRegexp), + } + box, err := c.Authenticate(req) + require.NoError(t, err) + require.EqualValues(t, expBox, box.AccessBox) +} + +func TestPresignRequestV4a(t *testing.T) { + var accessKeyAddr oid.Address + err := accessKeyAddr.DecodeString("8N7CYBY74kxZXoyvA5UNdmovaXqFpwNfvEPsqaN81es2/3tDwq5tR8fByrJcyJwyiuYX7Dae8tyDT7pd8oaL1MBto") + require.NoError(t, err) + + accessKeyID := strings.ReplaceAll(accessKeyAddr.String(), "/", "0") + secretKey := "713d0a0b9efc7d22923e17b0402a6a89b4273bc711c8bacb2da1b643d0006aeb" + + signer := v4a.NewSigner(func(options *v4a.SignerOptions) { + options.DisableURIPathEscaping = true + options.LogSigning = true + options.Logger = logging.NewStandardLogger(os.Stdout) + }) + + credAdapter := v4a.SymmetricCredentialAdaptor{ + SymmetricProvider: credentialsv2.NewStaticCredentialsProvider(accessKeyID, secretKey, ""), + } + + creds, err := credAdapter.RetrievePrivateKey(context.TODO()) + require.NoError(t, err) + + signingTime := time.Now() + service := "s3" + regionSet := []string{"spb"} + + req, err := http.NewRequest("GET", "http://localhost:8084/bucket/object", nil) + require.NoError(t, err) + //req.Header.Set(AmzRegionSet, strings.Join(regionSet, ",")) + //req.Header.Set(AmzDate, signingTime.Format("20060102T150405Z")) + //req.Header.Set(AmzAlgorithm, signaturePreambleSigV4A) + + presignedURL, hdr, err := signer.PresignHTTP(req.Context(), creds, req, "", service, regionSet, signingTime) + require.NoError(t, err) + + fmt.Println(presignedURL) + fmt.Println(hdr) + + signature := req.URL.Query().Get(AmzSignature) + + r, err := http.NewRequest("GET", presignedURL, nil) + require.NoError(t, err) + + err = signer.VerifySignature(creds, r, "", service, regionSet, signingTime, signature) + require.NoError(t, err) +} diff --git a/api/auth/signer/v4asdk2/credentials.go b/api/auth/signer/v4asdk2/credentials.go new file mode 100644 index 0000000..152482d --- /dev/null +++ b/api/auth/signer/v4asdk2/credentials.go @@ -0,0 +1,140 @@ +package v4a + +import ( + "context" + "crypto/ecdsa" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" +) + +// Credentials is Context, ECDSA, and Optional Session Token that can be used +// to sign requests using SigV4a +type Credentials struct { + Context string + PrivateKey *ecdsa.PrivateKey + SessionToken string + + // Time the credentials will expire. + CanExpire bool + Expires time.Time +} + +// Expired returns if the credentials have expired. +func (v Credentials) Expired() bool { + if v.CanExpire { + return !v.Expires.After(time.Now()) + } + + return false +} + +// HasKeys returns if the credentials keys are set. +func (v Credentials) HasKeys() bool { + return len(v.Context) > 0 && v.PrivateKey != nil +} + +// SymmetricCredentialAdaptor wraps a SigV4 AccessKey/SecretKey provider and adapts the credentials +// to a ECDSA PrivateKey for signing with SiV4a +type SymmetricCredentialAdaptor struct { + SymmetricProvider aws.CredentialsProvider + + asymmetric atomic.Value + m sync.Mutex +} + +// Retrieve retrieves symmetric credentials from the underlying provider. +func (s *SymmetricCredentialAdaptor) Retrieve(ctx context.Context) (aws.Credentials, error) { + symCreds, err := s.retrieveFromSymmetricProvider(ctx) + if err != nil { + return aws.Credentials{}, err + } + + if asymCreds := s.getCreds(); asymCreds == nil { + return symCreds, nil + } + + s.m.Lock() + defer s.m.Unlock() + + asymCreds := s.getCreds() + if asymCreds == nil { + return symCreds, nil + } + + // if the context does not match the access key id clear it + if asymCreds.Context != symCreds.AccessKeyID { + s.asymmetric.Store((*Credentials)(nil)) + } + + return symCreds, nil +} + +// RetrievePrivateKey returns credentials suitable for SigV4a signing +func (s *SymmetricCredentialAdaptor) RetrievePrivateKey(ctx context.Context) (Credentials, error) { + if asymCreds := s.getCreds(); asymCreds != nil { + return *asymCreds, nil + } + + s.m.Lock() + defer s.m.Unlock() + + if asymCreds := s.getCreds(); asymCreds != nil { + return *asymCreds, nil + } + + symmetricCreds, err := s.retrieveFromSymmetricProvider(ctx) + if err != nil { + return Credentials{}, fmt.Errorf("failed to retrieve symmetric credentials: %v", err) + } + + privateKey, err := deriveKeyFromAccessKeyPair(symmetricCreds.AccessKeyID, symmetricCreds.SecretAccessKey) + if err != nil { + return Credentials{}, fmt.Errorf("failed to derive assymetric key from credentials") + } + + creds := Credentials{ + Context: symmetricCreds.AccessKeyID, + PrivateKey: privateKey, + SessionToken: symmetricCreds.SessionToken, + CanExpire: symmetricCreds.CanExpire, + Expires: symmetricCreds.Expires, + } + + s.asymmetric.Store(&creds) + + return creds, nil +} + +func (s *SymmetricCredentialAdaptor) getCreds() *Credentials { + v := s.asymmetric.Load() + + if v == nil { + return nil + } + + c := v.(*Credentials) + if c != nil && c.HasKeys() && !c.Expired() { + return c + } + + return nil +} + +func (s *SymmetricCredentialAdaptor) retrieveFromSymmetricProvider(ctx context.Context) (aws.Credentials, error) { + credentials, err := s.SymmetricProvider.Retrieve(ctx) + if err != nil { + return aws.Credentials{}, err + } + + return credentials, nil +} + +// CredentialsProvider is the interface for a provider to retrieve credentials +// to sign requests with. +type CredentialsProvider interface { + RetrievePrivateKey(context.Context) (Credentials, error) +} diff --git a/api/auth/signer/v4asdk2/credentials_test.go b/api/auth/signer/v4asdk2/credentials_test.go new file mode 100644 index 0000000..bc89ec3 --- /dev/null +++ b/api/auth/signer/v4asdk2/credentials_test.go @@ -0,0 +1,77 @@ +package v4a + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" +) + +type rotatingCredsProvider struct { + count int + fail chan struct{} +} + +func (r *rotatingCredsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { + select { + case <-r.fail: + return aws.Credentials{}, fmt.Errorf("rotatingCredsProvider error") + default: + } + credentials := aws.Credentials{ + AccessKeyID: fmt.Sprintf("ACCESS_KEY_ID_%d", r.count), + SecretAccessKey: fmt.Sprintf("SECRET_ACCESS_KEY_%d", r.count), + SessionToken: fmt.Sprintf("SESSION_TOKEN_%d", r.count), + } + return credentials, nil +} + +func TestSymmetricCredentialAdaptor(t *testing.T) { + provider := &rotatingCredsProvider{ + count: 0, + fail: make(chan struct{}), + } + + adaptor := &SymmetricCredentialAdaptor{SymmetricProvider: provider} + + if symCreds, err := adaptor.Retrieve(context.Background()); err != nil { + t.Fatalf("expect no error, got %v", err) + } else if !symCreds.HasKeys() { + t.Fatalf("expect symmetric credentials to have keys") + } + + if load := adaptor.asymmetric.Load(); load != nil { + t.Errorf("expect asymmetric credentials to be nil") + } + + if asymCreds, err := adaptor.RetrievePrivateKey(context.Background()); err != nil { + t.Fatalf("expect no error, got %v", err) + } else if !asymCreds.HasKeys() { + t.Fatalf("expect asymmetric credentials to have keys") + } + + if _, err := adaptor.Retrieve(context.Background()); err != nil { + t.Fatalf("expect no error, got %v", err) + } + + if load := adaptor.asymmetric.Load(); load.(*Credentials) == nil { + t.Errorf("expect asymmetric credentials to be not nil") + } + + provider.count++ + + if _, err := adaptor.Retrieve(context.Background()); err != nil { + t.Fatalf("expect no error, got %v", err) + } + + if load := adaptor.asymmetric.Load(); load.(*Credentials) != nil { + t.Errorf("expect asymmetric credentials to be nil") + } + + close(provider.fail) // All requests to the original provider will now fail from this point-on. + _, err := adaptor.Retrieve(context.Background()) + if err == nil { + t.Error("expect error, got nil") + } +} diff --git a/api/auth/signer/v4asdk2/error.go b/api/auth/signer/v4asdk2/error.go new file mode 100644 index 0000000..380d174 --- /dev/null +++ b/api/auth/signer/v4asdk2/error.go @@ -0,0 +1,17 @@ +package v4a + +import "fmt" + +// SigningError indicates an error condition occurred while performing SigV4a signing +type SigningError struct { + Err error +} + +func (e *SigningError) Error() string { + return fmt.Sprintf("failed to sign request: %v", e.Err) +} + +// Unwrap returns the underlying error cause +func (e *SigningError) Unwrap() error { + return e.Err +} diff --git a/api/auth/signer/v4asdk2/internal/crypto/compare.go b/api/auth/signer/v4asdk2/internal/crypto/compare.go new file mode 100644 index 0000000..1d0f25f --- /dev/null +++ b/api/auth/signer/v4asdk2/internal/crypto/compare.go @@ -0,0 +1,30 @@ +package crypto + +import "fmt" + +// ConstantTimeByteCompare is a constant-time byte comparison of x and y. This function performs an absolute comparison +// if the two byte slices assuming they represent a big-endian number. +// +// error if len(x) != len(y) +// -1 if x < y +// 0 if x == y +// +1 if x > y +func ConstantTimeByteCompare(x, y []byte) (int, error) { + if len(x) != len(y) { + return 0, fmt.Errorf("slice lengths do not match") + } + + xLarger, yLarger := 0, 0 + + for i := 0; i < len(x); i++ { + xByte, yByte := int(x[i]), int(y[i]) + + x := ((yByte - xByte) >> 8) & 1 + y := ((xByte - yByte) >> 8) & 1 + + xLarger |= x &^ yLarger + yLarger |= y &^ xLarger + } + + return xLarger - yLarger, nil +} diff --git a/api/auth/signer/v4asdk2/internal/crypto/compare_test.go b/api/auth/signer/v4asdk2/internal/crypto/compare_test.go new file mode 100644 index 0000000..2bbdfdb --- /dev/null +++ b/api/auth/signer/v4asdk2/internal/crypto/compare_test.go @@ -0,0 +1,60 @@ +package crypto + +import ( + "bytes" + "math/big" + "testing" +) + +func TestConstantTimeByteCompare(t *testing.T) { + cases := []struct { + x, y []byte + r int + expectErr bool + }{ + {x: []byte{}, y: []byte{}, r: 0}, + {x: []byte{40}, y: []byte{30}, r: 1}, + {x: []byte{30}, y: []byte{40}, r: -1}, + {x: []byte{60, 40, 30, 10, 20}, y: []byte{50, 30, 20, 0, 10}, r: 1}, + {x: []byte{50, 30, 20, 0, 10}, y: []byte{60, 40, 30, 10, 20}, r: -1}, + {x: nil, y: []byte{}, r: 0}, + {x: []byte{}, y: nil, r: 0}, + {x: []byte{}, y: []byte{10}, expectErr: true}, + {x: []byte{10}, y: []byte{}, expectErr: true}, + {x: []byte{10, 20}, y: []byte{10}, expectErr: true}, + } + + for _, tt := range cases { + compare, err := ConstantTimeByteCompare(tt.x, tt.y) + if (err != nil) != tt.expectErr { + t.Fatalf("expectErr=%v, got %v", tt.expectErr, err) + } + if e, a := tt.r, compare; e != a { + t.Errorf("expect %v, got %v", e, a) + } + } +} + +func BenchmarkConstantTimeCompare(b *testing.B) { + x, y := big.NewInt(1023), big.NewInt(1024) + b.ResetTimer() + for i := 0; i < b.N; i++ { + ConstantTimeByteCompare(x.Bytes(), y.Bytes()) + } +} + +func BenchmarkCompare(b *testing.B) { + x, y := big.NewInt(1023).Bytes(), big.NewInt(1024).Bytes() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bytes.Compare(x, y) + } +} + +func mustBigInt(s string) *big.Int { + b, ok := (&big.Int{}).SetString(s, 16) + if !ok { + panic("can't parse as big.Int") + } + return b +} diff --git a/api/auth/signer/v4asdk2/internal/crypto/ecc.go b/api/auth/signer/v4asdk2/internal/crypto/ecc.go new file mode 100644 index 0000000..758c73f --- /dev/null +++ b/api/auth/signer/v4asdk2/internal/crypto/ecc.go @@ -0,0 +1,113 @@ +package crypto + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/hmac" + "encoding/asn1" + "encoding/binary" + "fmt" + "hash" + "math" + "math/big" +) + +type ecdsaSignature struct { + R, S *big.Int +} + +// ECDSAKey takes the given elliptic curve, and private key (d) byte slice +// and returns the private ECDSA key. +func ECDSAKey(curve elliptic.Curve, d []byte) *ecdsa.PrivateKey { + return ECDSAKeyFromPoint(curve, (&big.Int{}).SetBytes(d)) +} + +// ECDSAKeyFromPoint takes the given elliptic curve and point and returns the +// private and public keypair +func ECDSAKeyFromPoint(curve elliptic.Curve, d *big.Int) *ecdsa.PrivateKey { + pX, pY := curve.ScalarBaseMult(d.Bytes()) + + privKey := &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: curve, + X: pX, + Y: pY, + }, + D: d, + } + + return privKey +} + +// ECDSAPublicKey takes the provide curve and (x, y) coordinates and returns +// *ecdsa.PublicKey. Returns an error if the given points are not on the curve. +func ECDSAPublicKey(curve elliptic.Curve, x, y []byte) (*ecdsa.PublicKey, error) { + xPoint := (&big.Int{}).SetBytes(x) + yPoint := (&big.Int{}).SetBytes(y) + + if !curve.IsOnCurve(xPoint, yPoint) { + return nil, fmt.Errorf("point(%v, %v) is not on the given curve", xPoint.String(), yPoint.String()) + } + + return &ecdsa.PublicKey{ + Curve: curve, + X: xPoint, + Y: yPoint, + }, nil +} + +// VerifySignature takes the provided public key, hash, and asn1 encoded signature and returns +// whether the given signature is valid. +func VerifySignature(key *ecdsa.PublicKey, hash []byte, signature []byte) (bool, error) { + var ecdsaSignature ecdsaSignature + + _, err := asn1.Unmarshal(signature, &ecdsaSignature) + if err != nil { + return false, err + } + + return ecdsa.Verify(key, hash, ecdsaSignature.R, ecdsaSignature.S), nil +} + +// HMACKeyDerivation provides an implementation of a NIST-800-108 of a KDF (Key Derivation Function) in Counter Mode. +// For the purposes of this implantation HMAC is used as the PRF (Pseudorandom function), where the value of +// `r` is defined as a 4 byte counter. +func HMACKeyDerivation(hash func() hash.Hash, bitLen int, key []byte, label, context []byte) ([]byte, error) { + // verify that we won't overflow the counter + n := int64(math.Ceil((float64(bitLen) / 8) / float64(hash().Size()))) + if n > 0x7FFFFFFF { + return nil, fmt.Errorf("unable to derive key of size %d using 32-bit counter", bitLen) + } + + // verify the requested bit length is not larger then the length encoding size + if int64(bitLen) > 0x7FFFFFFF { + return nil, fmt.Errorf("bitLen is greater than 32-bits") + } + + fixedInput := bytes.NewBuffer(nil) + fixedInput.Write(label) + fixedInput.WriteByte(0x00) + fixedInput.Write(context) + if err := binary.Write(fixedInput, binary.BigEndian, int32(bitLen)); err != nil { + return nil, fmt.Errorf("failed to write bit length to fixed input string: %v", err) + } + + var output []byte + + h := hmac.New(hash, key) + + for i := int64(1); i <= n; i++ { + h.Reset() + if err := binary.Write(h, binary.BigEndian, int32(i)); err != nil { + return nil, err + } + _, err := h.Write(fixedInput.Bytes()) + if err != nil { + return nil, err + } + output = append(output, h.Sum(nil)...) + } + + return output[:bitLen/8], nil +} diff --git a/api/auth/signer/v4asdk2/internal/crypto/ecc_test.go b/api/auth/signer/v4asdk2/internal/crypto/ecc_test.go new file mode 100644 index 0000000..72a5e8d --- /dev/null +++ b/api/auth/signer/v4asdk2/internal/crypto/ecc_test.go @@ -0,0 +1,277 @@ +package crypto + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "io" + "testing" +) + +func TestECDSAPublicKeyDerivation_P256(t *testing.T) { + d := []byte{ + 0xc9, 0x80, 0x68, 0x98, 0xa0, 0x33, 0x49, 0x16, 0xc8, 0x60, 0x74, 0x88, 0x80, 0xa5, 0x41, 0xf0, + 0x93, 0xb5, 0x79, 0xa9, 0xb1, 0xf3, 0x29, 0x34, 0xd8, 0x6c, 0x36, 0x3c, 0x39, 0x80, 0x03, 0x57, + } + + x := []byte{ + 0xd0, 0x72, 0x0d, 0xc6, 0x91, 0xaa, 0x80, 0x09, 0x6b, 0xa3, 0x2f, 0xed, 0x1c, 0xb9, 0x7c, 0x2b, + 0x62, 0x06, 0x90, 0xd0, 0x6d, 0xe0, 0x31, 0x7b, 0x86, 0x18, 0xd5, 0xce, 0x65, 0xeb, 0x72, 0x8f, + } + + y := []byte{ + 0x96, 0x81, 0xb5, 0x17, 0xb1, 0xcd, 0xa1, 0x7d, 0x0d, 0x83, 0xd3, 0x35, 0xd9, 0xc4, 0xa8, 0xa9, + 0xa9, 0xb0, 0xb1, 0xb3, 0xc7, 0x10, 0x6d, 0x8f, 0x3c, 0x72, 0xbc, 0x50, 0x93, 0xdc, 0x27, 0x5f, + } + + testKeyDerivation(t, elliptic.P256(), d, x, y) +} + +func TestECDSAPublicKeyDerivation_P384(t *testing.T) { + d := []byte{ + 0x53, 0x94, 0xf7, 0x97, 0x3e, 0xa8, 0x68, 0xc5, 0x2b, 0xf3, 0xff, 0x8d, 0x8c, 0xee, 0xb4, 0xdb, + 0x90, 0xa6, 0x83, 0x65, 0x3b, 0x12, 0x48, 0x5d, 0x5f, 0x62, 0x7c, 0x3c, 0xe5, 0xab, 0xd8, 0x97, + 0x8f, 0xc9, 0x67, 0x3d, 0x14, 0xa7, 0x1d, 0x92, 0x57, 0x47, 0x93, 0x16, 0x62, 0x49, 0x3c, 0x37, + } + + x := []byte{ + 0xfd, 0x3c, 0x84, 0xe5, 0x68, 0x9b, 0xed, 0x27, 0x0e, 0x60, 0x1b, 0x3d, 0x80, 0xf9, 0x0d, 0x67, + 0xa9, 0xae, 0x45, 0x1c, 0xce, 0x89, 0x0f, 0x53, 0xe5, 0x83, 0x22, 0x9a, 0xd0, 0xe2, 0xee, 0x64, + 0x56, 0x11, 0xfa, 0x99, 0x36, 0xdf, 0xa4, 0x53, 0x06, 0xec, 0x18, 0x06, 0x67, 0x74, 0xaa, 0x24, + } + + y := []byte{ + 0xb8, 0x3c, 0xa4, 0x12, 0x6c, 0xfc, 0x4c, 0x4d, 0x1d, 0x18, 0xa4, 0xb6, 0xc2, 0x1c, 0x7f, 0x69, + 0x9d, 0x51, 0x23, 0xdd, 0x9c, 0x24, 0xf6, 0x6f, 0x83, 0x38, 0x46, 0xee, 0xb5, 0x82, 0x96, 0x19, + 0x6b, 0x42, 0xec, 0x06, 0x42, 0x5d, 0xb5, 0xb7, 0x0a, 0x4b, 0x81, 0xb7, 0xfc, 0xf7, 0x05, 0xa0, + } + + testKeyDerivation(t, elliptic.P384(), d, x, y) +} + +func TestECDSAKnownSigningValue_P256(t *testing.T) { + d := []byte{ + 0x51, 0x9b, 0x42, 0x3d, 0x71, 0x5f, 0x8b, 0x58, 0x1f, 0x4f, 0xa8, 0xee, 0x59, 0xf4, 0x77, 0x1a, + 0x5b, 0x44, 0xc8, 0x13, 0x0b, 0x4e, 0x3e, 0xac, 0xca, 0x54, 0xa5, 0x6d, 0xda, 0x72, 0xb4, 0x64, + } + + testKnownSigningValue(t, elliptic.P256(), d) +} + +func TestECDSAKnownSigningValue_P384(t *testing.T) { + d := []byte{ + 0x53, 0x94, 0xf7, 0x97, 0x3e, 0xa8, 0x68, 0xc5, 0x2b, 0xf3, 0xff, 0x8d, 0x8c, 0xee, 0xb4, 0xdb, + 0x90, 0xa6, 0x83, 0x65, 0x3b, 0x12, 0x48, 0x5d, 0x5f, 0x62, 0x7c, 0x3c, 0xe5, 0xab, 0xd8, 0x97, + 0x8f, 0xc9, 0x67, 0x3d, 0x14, 0xa7, 0x1d, 0x92, 0x57, 0x47, 0x93, 0x16, 0x62, 0x49, 0x3c, 0x37, + } + + testKnownSigningValue(t, elliptic.P384(), d) +} + +func testKeyDerivation(t *testing.T, curve elliptic.Curve, d, expectedX, expectedY []byte) { + privKey := ECDSAKey(curve, d) + + if e, a := d, privKey.D.Bytes(); bytes.Compare(e, a) != 0 { + t.Errorf("expected % x, got % x", e, a) + } + + if e, a := expectedX, privKey.X.Bytes(); bytes.Compare(e, a) != 0 { + t.Errorf("expected % x, got % x", e, a) + } + + if e, a := expectedY, privKey.Y.Bytes(); bytes.Compare(e, a) != 0 { + t.Errorf("expected % x, got % x", e, a) + } +} + +func testKnownSigningValue(t *testing.T, curve elliptic.Curve, d []byte) { + signingKey := ECDSAKey(curve, d) + + message := []byte{ + 0x59, 0x05, 0x23, 0x88, 0x77, 0xc7, 0x74, 0x21, 0xf7, 0x3e, 0x43, 0xee, 0x3d, 0xa6, 0xf2, 0xd9, + 0xe2, 0xcc, 0xad, 0x5f, 0xc9, 0x42, 0xdc, 0xec, 0x0c, 0xbd, 0x25, 0x48, 0x29, 0x35, 0xfa, 0xaf, + 0x41, 0x69, 0x83, 0xfe, 0x16, 0x5b, 0x1a, 0x04, 0x5e, 0xe2, 0xbc, 0xd2, 0xe6, 0xdc, 0xa3, 0xbd, + 0xf4, 0x6c, 0x43, 0x10, 0xa7, 0x46, 0x1f, 0x9a, 0x37, 0x96, 0x0c, 0xa6, 0x72, 0xd3, 0xfe, 0xb5, + 0x47, 0x3e, 0x25, 0x36, 0x05, 0xfb, 0x1d, 0xdf, 0xd2, 0x80, 0x65, 0xb5, 0x3c, 0xb5, 0x85, 0x8a, + 0x8a, 0xd2, 0x81, 0x75, 0xbf, 0x9b, 0xd3, 0x86, 0xa5, 0xe4, 0x71, 0xea, 0x7a, 0x65, 0xc1, 0x7c, + 0xc9, 0x34, 0xa9, 0xd7, 0x91, 0xe9, 0x14, 0x91, 0xeb, 0x37, 0x54, 0xd0, 0x37, 0x99, 0x79, 0x0f, + 0xe2, 0xd3, 0x08, 0xd1, 0x61, 0x46, 0xd5, 0xc9, 0xb0, 0xd0, 0xde, 0xbd, 0x97, 0xd7, 0x9c, 0xe8, + } + + sha256Hash := sha256.New() + _, err := io.Copy(sha256Hash, bytes.NewReader(message)) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + msgHash := sha256Hash.Sum(nil) + msgSignature, err := signingKey.Sign(rand.Reader, msgHash, crypto.SHA256) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + verified, err := VerifySignature(&signingKey.PublicKey, msgHash, msgSignature) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if !verified { + t.Fatalf("failed to verify message msgSignature") + } +} + +func TestECDSAInvalidSignature_P256(t *testing.T) { + testInvalidSignature(t, elliptic.P256()) +} + +func TestECDSAInvalidSignature_P384(t *testing.T) { + testInvalidSignature(t, elliptic.P384()) +} + +func TestECDSAGenKeySignature_P256(t *testing.T) { + testGenKeySignature(t, elliptic.P256()) +} + +func TestECDSAGenKeySignature_P384(t *testing.T) { + testGenKeySignature(t, elliptic.P384()) +} + +func testInvalidSignature(t *testing.T, curve elliptic.Curve) { + privateKey, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + t.Fatalf("failed to generate key: %v", err) + } + + message := []byte{ + 0x59, 0x05, 0x23, 0x88, 0x77, 0xc7, 0x74, 0x21, 0xf7, 0x3e, 0x43, 0xee, 0x3d, 0xa6, 0xf2, 0xd9, + 0xe2, 0xcc, 0xad, 0x5f, 0xc9, 0x42, 0xdc, 0xec, 0x0c, 0xbd, 0x25, 0x48, 0x29, 0x35, 0xfa, 0xaf, + 0x41, 0x69, 0x83, 0xfe, 0x16, 0x5b, 0x1a, 0x04, 0x5e, 0xe2, 0xbc, 0xd2, 0xe6, 0xdc, 0xa3, 0xbd, + 0xf4, 0x6c, 0x43, 0x10, 0xa7, 0x46, 0x1f, 0x9a, 0x37, 0x96, 0x0c, 0xa6, 0x72, 0xd3, 0xfe, 0xb5, + 0x47, 0x3e, 0x25, 0x36, 0x05, 0xfb, 0x1d, 0xdf, 0xd2, 0x80, 0x65, 0xb5, 0x3c, 0xb5, 0x85, 0x8a, + 0x8a, 0xd2, 0x81, 0x75, 0xbf, 0x9b, 0xd3, 0x86, 0xa5, 0xe4, 0x71, 0xea, 0x7a, 0x65, 0xc1, 0x7c, + 0xc9, 0x34, 0xa9, 0xd7, 0x91, 0xe9, 0x14, 0x91, 0xeb, 0x37, 0x54, 0xd0, 0x37, 0x99, 0x79, 0x0f, + 0xe2, 0xd3, 0x08, 0xd1, 0x61, 0x46, 0xd5, 0xc9, 0xb0, 0xd0, 0xde, 0xbd, 0x97, 0xd7, 0x9c, 0xe8, + } + + sha256Hash := sha256.New() + _, err = io.Copy(sha256Hash, bytes.NewReader(message)) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + msgHash := sha256Hash.Sum(nil) + msgSignature, err := privateKey.Sign(rand.Reader, msgHash, crypto.SHA256) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + byteToFlip := 15 + switch msgSignature[byteToFlip] { + case 0: + msgSignature[byteToFlip] = 0x0a + default: + msgSignature[byteToFlip] &^= msgSignature[byteToFlip] + } + + verified, err := VerifySignature(&privateKey.PublicKey, msgHash, msgSignature) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if verified { + t.Fatalf("expected message verification to fail") + } +} + +func testGenKeySignature(t *testing.T, curve elliptic.Curve) { + privateKey, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + t.Fatalf("failed to generate key: %v", err) + } + + message := []byte{ + 0x59, 0x05, 0x23, 0x88, 0x77, 0xc7, 0x74, 0x21, 0xf7, 0x3e, 0x43, 0xee, 0x3d, 0xa6, 0xf2, 0xd9, + 0xe2, 0xcc, 0xad, 0x5f, 0xc9, 0x42, 0xdc, 0xec, 0x0c, 0xbd, 0x25, 0x48, 0x29, 0x35, 0xfa, 0xaf, + 0x41, 0x69, 0x83, 0xfe, 0x16, 0x5b, 0x1a, 0x04, 0x5e, 0xe2, 0xbc, 0xd2, 0xe6, 0xdc, 0xa3, 0xbd, + 0xf4, 0x6c, 0x43, 0x10, 0xa7, 0x46, 0x1f, 0x9a, 0x37, 0x96, 0x0c, 0xa6, 0x72, 0xd3, 0xfe, 0xb5, + 0x47, 0x3e, 0x25, 0x36, 0x05, 0xfb, 0x1d, 0xdf, 0xd2, 0x80, 0x65, 0xb5, 0x3c, 0xb5, 0x85, 0x8a, + 0x8a, 0xd2, 0x81, 0x75, 0xbf, 0x9b, 0xd3, 0x86, 0xa5, 0xe4, 0x71, 0xea, 0x7a, 0x65, 0xc1, 0x7c, + 0xc9, 0x34, 0xa9, 0xd7, 0x91, 0xe9, 0x14, 0x91, 0xeb, 0x37, 0x54, 0xd0, 0x37, 0x99, 0x79, 0x0f, + 0xe2, 0xd3, 0x08, 0xd1, 0x61, 0x46, 0xd5, 0xc9, 0xb0, 0xd0, 0xde, 0xbd, 0x97, 0xd7, 0x9c, 0xe8, + } + + sha256Hash := sha256.New() + _, err = io.Copy(sha256Hash, bytes.NewReader(message)) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + msgHash := sha256Hash.Sum(nil) + msgSignature, err := privateKey.Sign(rand.Reader, msgHash, crypto.SHA256) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + verified, err := VerifySignature(&privateKey.PublicKey, msgHash, msgSignature) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if !verified { + t.Fatalf("expected message verification to fail") + } +} + +func TestECDSASignatureFormat(t *testing.T) { + asn1Signature := []byte{ + 0x30, 0x45, 0x02, 0x21, 0x00, 0xd7, 0xc5, 0xb9, 0x9e, 0x0b, 0xb1, 0x1a, 0x1f, 0x32, 0xda, 0x66, 0xe0, 0xff, + 0x59, 0xb7, 0x8a, 0x5e, 0xb3, 0x94, 0x9c, 0x23, 0xb3, 0xfc, 0x1f, 0x18, 0xcc, 0xf6, 0x61, 0x67, 0x8b, 0xf1, + 0xc1, 0x02, 0x20, 0x26, 0x4d, 0x8b, 0x7c, 0xaa, 0x52, 0x4c, 0xc0, 0x2e, 0x5f, 0xf6, 0x7e, 0x24, 0x82, 0xe5, + 0xfb, 0xcb, 0xc7, 0x9b, 0x83, 0x0d, 0x19, 0x7e, 0x7a, 0x40, 0x37, 0x87, 0xdd, 0x1c, 0x93, 0x13, 0xc4, + } + + x := []byte{ + 0x1c, 0xcb, 0xe9, 0x1c, 0x07, 0x5f, 0xc7, 0xf4, 0xf0, 0x33, 0xbf, 0xa2, 0x48, 0xdb, 0x8f, 0xcc, + 0xd3, 0x56, 0x5d, 0xe9, 0x4b, 0xbf, 0xb1, 0x2f, 0x3c, 0x59, 0xff, 0x46, 0xc2, 0x71, 0xbf, 0x83, + } + + y := []byte{ + 0xce, 0x40, 0x14, 0xc6, 0x88, 0x11, 0xf9, 0xa2, 0x1a, 0x1f, 0xdb, 0x2c, 0x0e, 0x61, 0x13, 0xe0, + 0x6d, 0xb7, 0xca, 0x93, 0xb7, 0x40, 0x4e, 0x78, 0xdc, 0x7c, 0xcd, 0x5c, 0xa8, 0x9a, 0x4c, 0xa9, + } + + publicKey, err := ECDSAPublicKey(elliptic.P256(), x, y) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + message := []byte{ + 0x59, 0x05, 0x23, 0x88, 0x77, 0xc7, 0x74, 0x21, 0xf7, 0x3e, 0x43, 0xee, 0x3d, 0xa6, 0xf2, 0xd9, + 0xe2, 0xcc, 0xad, 0x5f, 0xc9, 0x42, 0xdc, 0xec, 0x0c, 0xbd, 0x25, 0x48, 0x29, 0x35, 0xfa, 0xaf, + 0x41, 0x69, 0x83, 0xfe, 0x16, 0x5b, 0x1a, 0x04, 0x5e, 0xe2, 0xbc, 0xd2, 0xe6, 0xdc, 0xa3, 0xbd, + 0xf4, 0x6c, 0x43, 0x10, 0xa7, 0x46, 0x1f, 0x9a, 0x37, 0x96, 0x0c, 0xa6, 0x72, 0xd3, 0xfe, 0xb5, + 0x47, 0x3e, 0x25, 0x36, 0x05, 0xfb, 0x1d, 0xdf, 0xd2, 0x80, 0x65, 0xb5, 0x3c, 0xb5, 0x85, 0x8a, + 0x8a, 0xd2, 0x81, 0x75, 0xbf, 0x9b, 0xd3, 0x86, 0xa5, 0xe4, 0x71, 0xea, 0x7a, 0x65, 0xc1, 0x7c, + 0xc9, 0x34, 0xa9, 0xd7, 0x91, 0xe9, 0x14, 0x91, 0xeb, 0x37, 0x54, 0xd0, 0x37, 0x99, 0x79, 0x0f, + 0xe2, 0xd3, 0x08, 0xd1, 0x61, 0x46, 0xd5, 0xc9, 0xb0, 0xd0, 0xde, 0xbd, 0x97, 0xd7, 0x9c, 0xe8, + } + + hash := sha256.New() + _, err = io.Copy(hash, bytes.NewReader(message)) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + msgHash := hash.Sum(nil) + + verifySignature, err := VerifySignature(publicKey, msgHash, asn1Signature) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if !verifySignature { + t.Fatalf("failed to verify signature") + } +} diff --git a/api/auth/signer/v4asdk2/internal/v4/const.go b/api/auth/signer/v4asdk2/internal/v4/const.go new file mode 100644 index 0000000..89a76e2 --- /dev/null +++ b/api/auth/signer/v4asdk2/internal/v4/const.go @@ -0,0 +1,36 @@ +package v4 + +const ( + // EmptyStringSHA256 is the hex encoded sha256 value of an empty string + EmptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` + + // UnsignedPayload indicates that the request payload body is unsigned + UnsignedPayload = "UNSIGNED-PAYLOAD" + + // AmzAlgorithmKey indicates the signing algorithm + AmzAlgorithmKey = "X-Amz-Algorithm" + + // AmzSecurityTokenKey indicates the security token to be used with temporary credentials + AmzSecurityTokenKey = "X-Amz-Security-Token" + + // AmzDateKey is the UTC timestamp for the request in the format YYYYMMDD'T'HHMMSS'Z' + AmzDateKey = "X-Amz-Date" + + // AmzCredentialKey is the access key ID and credential scope + AmzCredentialKey = "X-Amz-Credential" + + // AmzSignedHeadersKey is the set of headers signed for the request + AmzSignedHeadersKey = "X-Amz-SignedHeaders" + + // AmzSignatureKey is the query parameter to store the SigV4 signature + AmzSignatureKey = "X-Amz-Signature" + + // TimeFormat is the time format to be used in the X-Amz-Date header or query parameter + TimeFormat = "20060102T150405Z" + + // ShortTimeFormat is the shorten time format used in the credential scope + ShortTimeFormat = "20060102" + + // ContentSHAKey is the SHA256 of request body + ContentSHAKey = "X-Amz-Content-Sha256" +) diff --git a/api/auth/signer/v4asdk2/internal/v4/header_rules.go b/api/auth/signer/v4asdk2/internal/v4/header_rules.go new file mode 100644 index 0000000..7a9d415 --- /dev/null +++ b/api/auth/signer/v4asdk2/internal/v4/header_rules.go @@ -0,0 +1,88 @@ +package v4 + +import ( + "strings" +) + +// Rules houses a set of Rule needed for validation of a +// string value +type Rules []Rule + +// Rule interface allows for more flexible rules and just simply +// checks whether or not a value adheres to that Rule +type Rule interface { + IsValid(value string) bool +} + +// IsValid will iterate through all rules and see if any rules +// apply to the value and supports nested rules +func (r Rules) IsValid(value string) bool { + for _, rule := range r { + if rule.IsValid(value) { + return true + } + } + return false +} + +// MapRule generic Rule for maps +type MapRule map[string]struct{} + +// IsValid for the map Rule satisfies whether it exists in the map +func (m MapRule) IsValid(value string) bool { + _, ok := m[value] + return ok +} + +// AllowList is a generic Rule for whitelisting +type AllowList struct { + Rule +} + +// IsValid for AllowList checks if the value is within the AllowList +func (w AllowList) IsValid(value string) bool { + return w.Rule.IsValid(value) +} + +// DenyList is a generic Rule for blacklisting +type DenyList struct { + Rule +} + +// IsValid for AllowList checks if the value is within the AllowList +func (b DenyList) IsValid(value string) bool { + return !b.Rule.IsValid(value) +} + +// Patterns is a list of strings to match against +type Patterns []string + +// IsValid for Patterns checks each pattern and returns if a match has +// been found +func (p Patterns) IsValid(value string) bool { + for _, pattern := range p { + if HasPrefixFold(value, pattern) { + return true + } + } + return false +} + +// InclusiveRules rules allow for rules to depend on one another +type InclusiveRules []Rule + +// IsValid will return true if all rules are true +func (r InclusiveRules) IsValid(value string) bool { + for _, rule := range r { + if !rule.IsValid(value) { + return false + } + } + return true +} + +// HasPrefixFold tests whether the string s begins with prefix, interpreted as UTF-8 strings, +// under Unicode case-folding. +func HasPrefixFold(s, prefix string) bool { + return len(s) >= len(prefix) && strings.EqualFold(s[0:len(prefix)], prefix) +} diff --git a/api/auth/signer/v4asdk2/internal/v4/headers.go b/api/auth/signer/v4asdk2/internal/v4/headers.go new file mode 100644 index 0000000..f6fc7f6 --- /dev/null +++ b/api/auth/signer/v4asdk2/internal/v4/headers.go @@ -0,0 +1,79 @@ +package v4 + +// IgnoredPresignedHeaders is a list of headers that are ignored during signing +var IgnoredPresignedHeaders = Rules{ + DenyList{ + MapRule{ + "Authorization": struct{}{}, + "User-Agent": struct{}{}, + "X-Amzn-Trace-Id": struct{}{}, + }, + }, +} + +// IgnoredHeaders is a list of headers that are ignored during signing +// drop User-Agent header to be compatible with aws sdk java v1. +var IgnoredHeaders = Rules{ + DenyList{ + MapRule{ + "Authorization": struct{}{}, + //"User-Agent": struct{}{}, + "X-Amzn-Trace-Id": struct{}{}, + }, + }, +} + +// RequiredSignedHeaders is a whitelist for Build canonical headers. +var RequiredSignedHeaders = Rules{ + AllowList{ + MapRule{ + "Cache-Control": struct{}{}, + "Content-Disposition": struct{}{}, + "Content-Encoding": struct{}{}, + "Content-Language": struct{}{}, + "Content-Md5": struct{}{}, + "Content-Type": struct{}{}, + "Expires": struct{}{}, + "If-Match": struct{}{}, + "If-Modified-Since": struct{}{}, + "If-None-Match": struct{}{}, + "If-Unmodified-Since": struct{}{}, + "Range": struct{}{}, + "X-Amz-Acl": struct{}{}, + "X-Amz-Copy-Source": struct{}{}, + "X-Amz-Copy-Source-If-Match": struct{}{}, + "X-Amz-Copy-Source-If-Modified-Since": struct{}{}, + "X-Amz-Copy-Source-If-None-Match": struct{}{}, + "X-Amz-Copy-Source-If-Unmodified-Since": struct{}{}, + "X-Amz-Copy-Source-Range": struct{}{}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": struct{}{}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": struct{}{}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": struct{}{}, + "X-Amz-Grant-Full-control": struct{}{}, + "X-Amz-Grant-Read": struct{}{}, + "X-Amz-Grant-Read-Acp": struct{}{}, + "X-Amz-Grant-Write": struct{}{}, + "X-Amz-Grant-Write-Acp": struct{}{}, + "X-Amz-Metadata-Directive": struct{}{}, + "X-Amz-Mfa": struct{}{}, + "X-Amz-Request-Payer": struct{}{}, + "X-Amz-Server-Side-Encryption": struct{}{}, + "X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": struct{}{}, + "X-Amz-Server-Side-Encryption-Customer-Algorithm": struct{}{}, + "X-Amz-Server-Side-Encryption-Customer-Key": struct{}{}, + "X-Amz-Server-Side-Encryption-Customer-Key-Md5": struct{}{}, + "X-Amz-Storage-Class": struct{}{}, + "X-Amz-Website-Redirect-Location": struct{}{}, + "X-Amz-Content-Sha256": struct{}{}, + "X-Amz-Tagging": struct{}{}, + }, + }, + Patterns{"X-Amz-Meta-"}, +} + +// AllowedQueryHoisting is a whitelist for Build query headers. The boolean value +// represents whether or not it is a pattern. +var AllowedQueryHoisting = InclusiveRules{ + DenyList{RequiredSignedHeaders}, + Patterns{"X-Amz-"}, +} diff --git a/api/auth/signer/v4asdk2/internal/v4/hmac.go b/api/auth/signer/v4asdk2/internal/v4/hmac.go new file mode 100644 index 0000000..e7fa7a1 --- /dev/null +++ b/api/auth/signer/v4asdk2/internal/v4/hmac.go @@ -0,0 +1,13 @@ +package v4 + +import ( + "crypto/hmac" + "crypto/sha256" +) + +// HMACSHA256 computes a HMAC-SHA256 of data given the provided key. +func HMACSHA256(key []byte, data []byte) []byte { + hash := hmac.New(sha256.New, key) + hash.Write(data) + return hash.Sum(nil) +} diff --git a/api/auth/signer/v4asdk2/internal/v4/host.go b/api/auth/signer/v4asdk2/internal/v4/host.go new file mode 100644 index 0000000..bf93659 --- /dev/null +++ b/api/auth/signer/v4asdk2/internal/v4/host.go @@ -0,0 +1,75 @@ +package v4 + +import ( + "net/http" + "strings" +) + +// SanitizeHostForHeader removes default port from host and updates request.Host +func SanitizeHostForHeader(r *http.Request) { + host := getHost(r) + port := portOnly(host) + if port != "" && isDefaultPort(r.URL.Scheme, port) { + r.Host = stripPort(host) + } +} + +// Returns host from request +func getHost(r *http.Request) string { + if r.Host != "" { + return r.Host + } + + return r.URL.Host +} + +// Hostname returns u.Host, without any port number. +// +// If Host is an IPv6 literal with a port number, Hostname returns the +// IPv6 literal without the square brackets. IPv6 literals may include +// a zone identifier. +// +// Copied from the Go 1.8 standard library (net/url) +func stripPort(hostport string) string { + colon := strings.IndexByte(hostport, ':') + if colon == -1 { + return hostport + } + if i := strings.IndexByte(hostport, ']'); i != -1 { + return strings.TrimPrefix(hostport[:i], "[") + } + return hostport[:colon] +} + +// Port returns the port part of u.Host, without the leading colon. +// If u.Host doesn't contain a port, Port returns an empty string. +// +// Copied from the Go 1.8 standard library (net/url) +func portOnly(hostport string) string { + colon := strings.IndexByte(hostport, ':') + if colon == -1 { + return "" + } + if i := strings.Index(hostport, "]:"); i != -1 { + return hostport[i+len("]:"):] + } + if strings.Contains(hostport, "]") { + return "" + } + return hostport[colon+len(":"):] +} + +// Returns true if the specified URI is using the standard port +// (i.e. port 80 for HTTP URIs or 443 for HTTPS URIs) +func isDefaultPort(scheme, port string) bool { + if port == "" { + return true + } + + lowerCaseScheme := strings.ToLower(scheme) + if (lowerCaseScheme == "http" && port == "80") || (lowerCaseScheme == "https" && port == "443") { + return true + } + + return false +} diff --git a/api/auth/signer/v4asdk2/internal/v4/time.go b/api/auth/signer/v4asdk2/internal/v4/time.go new file mode 100644 index 0000000..1de06a7 --- /dev/null +++ b/api/auth/signer/v4asdk2/internal/v4/time.go @@ -0,0 +1,36 @@ +package v4 + +import "time" + +// SigningTime provides a wrapper around a time.Time which provides cached values for SigV4 signing. +type SigningTime struct { + time.Time + timeFormat string + shortTimeFormat string +} + +// NewSigningTime creates a new SigningTime given a time.Time +func NewSigningTime(t time.Time) SigningTime { + return SigningTime{ + Time: t, + } +} + +// TimeFormat provides a time formatted in the X-Amz-Date format. +func (m *SigningTime) TimeFormat() string { + return m.format(&m.timeFormat, TimeFormat) +} + +// ShortTimeFormat provides a time formatted of 20060102. +func (m *SigningTime) ShortTimeFormat() string { + return m.format(&m.shortTimeFormat, ShortTimeFormat) +} + +func (m *SigningTime) format(target *string, format string) string { + if len(*target) > 0 { + return *target + } + v := m.Time.Format(format) + *target = v + return v +} diff --git a/api/auth/signer/v4asdk2/internal/v4/util.go b/api/auth/signer/v4asdk2/internal/v4/util.go new file mode 100644 index 0000000..741019b --- /dev/null +++ b/api/auth/signer/v4asdk2/internal/v4/util.go @@ -0,0 +1,64 @@ +package v4 + +import ( + "net/url" + "strings" +) + +const doubleSpace = " " + +// StripExcessSpaces will rewrite the passed in slice's string values to not +// contain muliple side-by-side spaces. +func StripExcessSpaces(str string) string { + var j, k, l, m, spaces int + // Trim trailing spaces + for j = len(str) - 1; j >= 0 && str[j] == ' '; j-- { + } + + // Trim leading spaces + for k = 0; k < j && str[k] == ' '; k++ { + } + str = str[k : j+1] + + // Strip multiple spaces. + j = strings.Index(str, doubleSpace) + if j < 0 { + return str + } + + buf := []byte(str) + for k, m, l = j, j, len(buf); k < l; k++ { + if buf[k] == ' ' { + if spaces == 0 { + // First space. + buf[m] = buf[k] + m++ + } + spaces++ + } else { + // End of multiple spaces. + spaces = 0 + buf[m] = buf[k] + m++ + } + } + + return string(buf[:m]) +} + +// GetURIPath returns the escaped URI component from the provided URL +func GetURIPath(u *url.URL) string { + var uri string + + if len(u.Opaque) > 0 { + uri = "/" + strings.Join(strings.Split(u.Opaque, "/")[3:], "/") + } else { + uri = u.EscapedPath() + } + + if len(uri) == 0 { + uri = "/" + } + + return uri +} diff --git a/api/auth/signer/v4asdk2/internal/v4/util_test.go b/api/auth/signer/v4asdk2/internal/v4/util_test.go new file mode 100644 index 0000000..c29c1fa --- /dev/null +++ b/api/auth/signer/v4asdk2/internal/v4/util_test.go @@ -0,0 +1,75 @@ +package v4 + +import ( + "testing" +) + +func TestStripExcessHeaders(t *testing.T) { + vals := []string{ + "", + "123", + "1 2 3", + "1 2 3 ", + " 1 2 3", + "1 2 3", + "1 23", + "1 2 3", + "1 2 ", + " 1 2 ", + "12 3", + "12 3 1", + "12 3 1", + "12 3 1abc123", + } + + expected := []string{ + "", + "123", + "1 2 3", + "1 2 3", + "1 2 3", + "1 2 3", + "1 23", + "1 2 3", + "1 2", + "1 2", + "12 3", + "12 3 1", + "12 3 1", + "12 3 1abc123", + } + + for i := 0; i < len(vals); i++ { + r := StripExcessSpaces(vals[i]) + if e, a := expected[i], r; e != a { + t.Errorf("%d, expect %v, got %v", i, e, a) + } + } +} + +var stripExcessSpaceCases = []string{ + `AWS4-HMAC-SHA256 Credential=AKIDFAKEIDFAKEID/20160628/us-west-2/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=1234567890abcdef1234567890abcdef1234567890abcdef`, + `123 321 123 321`, + ` 123 321 123 321 `, + ` 123 321 123 321 `, + "123", + "1 2 3", + " 1 2 3", + "1 2 3", + "1 23", + "1 2 3", + "1 2 ", + " 1 2 ", + "12 3", + "12 3 1", + "12 3 1", + "12 3 1abc123", +} + +func BenchmarkStripExcessSpaces(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, v := range stripExcessSpaceCases { + StripExcessSpaces(v) + } + } +} diff --git a/api/auth/signer/v4asdk2/middleware.go b/api/auth/signer/v4asdk2/middleware.go new file mode 100644 index 0000000..e1b6612 --- /dev/null +++ b/api/auth/signer/v4asdk2/middleware.go @@ -0,0 +1,118 @@ +package v4a + +import ( + "context" + "fmt" + "net/http" + "time" + + awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + "github.com/aws/smithy-go/middleware" + smithyhttp "github.com/aws/smithy-go/transport/http" +) + +// HTTPSigner is SigV4a HTTP signer implementation +type HTTPSigner interface { + SignHTTP(ctx context.Context, credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, optfns ...func(*SignerOptions)) error +} + +// SignHTTPRequestMiddlewareOptions is the middleware options for constructing a SignHTTPRequestMiddleware. +type SignHTTPRequestMiddlewareOptions struct { + Credentials CredentialsProvider + Signer HTTPSigner + LogSigning bool +} + +// SignHTTPRequestMiddleware is a middleware for signing an HTTP request using SigV4a. +type SignHTTPRequestMiddleware struct { + credentials CredentialsProvider + signer HTTPSigner + logSigning bool +} + +// NewSignHTTPRequestMiddleware constructs a SignHTTPRequestMiddleware using the given SignHTTPRequestMiddlewareOptions. +func NewSignHTTPRequestMiddleware(options SignHTTPRequestMiddlewareOptions) *SignHTTPRequestMiddleware { + return &SignHTTPRequestMiddleware{ + credentials: options.Credentials, + signer: options.Signer, + logSigning: options.LogSigning, + } +} + +// ID the middleware identifier. +func (s *SignHTTPRequestMiddleware) ID() string { + return "Signing" +} + +// HandleFinalize signs an HTTP request using SigV4a. +func (s *SignHTTPRequestMiddleware) HandleFinalize( + ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler, +) ( + out middleware.FinalizeOutput, metadata middleware.Metadata, err error, +) { + if !hasCredentialProvider(s.credentials) { + return next.HandleFinalize(ctx, in) + } + + req, ok := in.Request.(*smithyhttp.Request) + if !ok { + return out, metadata, fmt.Errorf("unexpected request middleware type %T", in.Request) + } + + signingName, signingRegion := awsmiddleware.GetSigningName(ctx), awsmiddleware.GetSigningRegion(ctx) + payloadHash := v4.GetPayloadHash(ctx) + if len(payloadHash) == 0 { + return out, metadata, &SigningError{Err: fmt.Errorf("computed payload hash missing from context")} + } + + credentials, err := s.credentials.RetrievePrivateKey(ctx) + if err != nil { + return out, metadata, &SigningError{Err: fmt.Errorf("failed to retrieve credentials: %w", err)} + } + + signerOptions := []func(o *SignerOptions){ + func(o *SignerOptions) { + o.Logger = middleware.GetLogger(ctx) + o.LogSigning = s.logSigning + }, + } + + // existing DisableURIPathEscaping is equivalent in purpose + // to authentication scheme property DisableDoubleEncoding + //disableDoubleEncoding, overridden := internalauth.GetDisableDoubleEncoding(ctx) // internalauth "github.com/aws/aws-sdk-go-v2/internal/auth" + //if overridden { + // signerOptions = append(signerOptions, func(o *SignerOptions) { + // o.DisableURIPathEscaping = disableDoubleEncoding + // }) + //} + + err = s.signer.SignHTTP(ctx, credentials, req.Request, payloadHash, signingName, []string{signingRegion}, time.Now().UTC(), signerOptions...) + if err != nil { + return out, metadata, &SigningError{Err: fmt.Errorf("failed to sign http request, %w", err)} + } + + return next.HandleFinalize(ctx, in) +} + +func hasCredentialProvider(p CredentialsProvider) bool { + if p == nil { + return false + } + + return true +} + +// RegisterSigningMiddleware registers the SigV4a signing middleware to the stack. If a signing middleware is already +// present, this provided middleware will be swapped. Otherwise the middleware will be added at the tail of the +// finalize step. +func RegisterSigningMiddleware(stack *middleware.Stack, signingMiddleware *SignHTTPRequestMiddleware) (err error) { + const signedID = "Signing" + _, present := stack.Finalize.Get(signedID) + if present { + _, err = stack.Finalize.Swap(signedID, signingMiddleware) + } else { + err = stack.Finalize.Add(signingMiddleware, middleware.After) + } + return err +} diff --git a/api/auth/signer/v4asdk2/middleware_test.go b/api/auth/signer/v4asdk2/middleware_test.go new file mode 100644 index 0000000..135a5c5 --- /dev/null +++ b/api/auth/signer/v4asdk2/middleware_test.go @@ -0,0 +1,149 @@ +package v4a + +import ( + "bytes" + "context" + "errors" + "fmt" + "net/http" + "strings" + "testing" + "time" + + awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + "github.com/aws/smithy-go/logging" + "github.com/aws/smithy-go/middleware" + smithyhttp "github.com/aws/smithy-go/transport/http" +) + +type stubCredentialsProviderFunc func(context.Context) (Credentials, error) + +func (f stubCredentialsProviderFunc) RetrievePrivateKey(ctx context.Context) (Credentials, error) { + return f(ctx) +} + +type httpSignerFunc func(ctx context.Context, credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, optFns ...func(*SignerOptions)) error + +func (f httpSignerFunc) SignHTTP(ctx context.Context, credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, optFns ...func(*SignerOptions)) error { + return f(ctx, credentials, r, payloadHash, service, regionSet, signingTime, optFns...) +} + +func TestSignHTTPRequestMiddleware(t *testing.T) { + cases := map[string]struct { + creds CredentialsProvider + hash string + logSigning bool + expectedErr interface{} + }{ + "success": { + creds: stubCredentials, + hash: "0123456789abcdef", + }, + "error": { + creds: stubCredentialsProviderFunc(func(ctx context.Context) (Credentials, error) { + return Credentials{}, fmt.Errorf("credential error") + }), + hash: "", + expectedErr: &SigningError{}, + }, + "nil creds": { + creds: nil, + }, + "with log signing": { + creds: stubCredentials, + hash: "0123456789abcdef", + logSigning: true, + }, + } + + const ( + signingName = "serviceId" + signingRegion = "regionName" + ) + + for name, tt := range cases { + t.Run(name, func(t *testing.T) { + c := &SignHTTPRequestMiddleware{ + credentials: tt.creds, + signer: httpSignerFunc( + func(ctx context.Context, + credentials Credentials, r *http.Request, payloadHash string, + service string, regionSet []string, signingTime time.Time, + optFns ...func(*SignerOptions), + ) error { + var options SignerOptions + for _, fn := range optFns { + fn(&options) + } + if options.Logger == nil { + t.Errorf("expect logger, got none") + } + if options.LogSigning { + options.Logger.Logf(logging.Debug, t.Name()) + } + + expectCreds, _ := tt.creds.RetrievePrivateKey(ctx) + if diff := cmpDiff(expectCreds, credentials); len(diff) > 0 { + t.Error(diff) + } + if e, a := tt.hash, payloadHash; e != a { + t.Errorf("expected %v, got %v", e, a) + } + if e, a := signingName, service; e != a { + t.Errorf("expected %v, got %v", e, a) + } + if diff := cmpDiff([]string{signingRegion}, regionSet); len(diff) > 0 { + t.Error(diff) + } + return nil + }), + logSigning: tt.logSigning, + } + + next := middleware.FinalizeHandlerFunc(func(ctx context.Context, in middleware.FinalizeInput) (out middleware.FinalizeOutput, metadata middleware.Metadata, err error) { + return out, metadata, err + }) + + ctx := awsmiddleware.SetSigningRegion( + awsmiddleware.SetSigningName(context.Background(), signingName), + signingRegion) + + var loggerBuf bytes.Buffer + logger := logging.NewStandardLogger(&loggerBuf) + ctx = middleware.SetLogger(ctx, logger) + + if len(tt.hash) != 0 { + ctx = v4.SetPayloadHash(ctx, tt.hash) + } + + _, _, err := c.HandleFinalize(ctx, middleware.FinalizeInput{ + Request: &smithyhttp.Request{Request: &http.Request{}}, + }, next) + if err != nil && tt.expectedErr == nil { + t.Errorf("expected no error, got %v", err) + } else if err != nil && tt.expectedErr != nil { + e, a := tt.expectedErr, err + if !errors.As(a, &e) { + t.Errorf("expected error type %T, got %T", e, a) + } + } else if err == nil && tt.expectedErr != nil { + t.Errorf("expected error, got nil") + } + + if tt.logSigning { + if e, a := t.Name(), loggerBuf.String(); !strings.Contains(a, e) { + t.Errorf("expect %v logged in %v", e, a) + } + } else { + if loggerBuf.Len() != 0 { + t.Errorf("expect no log, got %v", loggerBuf.String()) + } + } + }) + } +} + +var ( + _ middleware.FinalizeMiddleware = &SignHTTPRequestMiddleware{} +) diff --git a/api/auth/signer/v4asdk2/presign_middleware.go b/api/auth/signer/v4asdk2/presign_middleware.go new file mode 100644 index 0000000..ecb0f9e --- /dev/null +++ b/api/auth/signer/v4asdk2/presign_middleware.go @@ -0,0 +1,116 @@ +package v4a + +import ( + "context" + "fmt" + "net/http" + "time" + + awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + "github.com/aws/smithy-go/middleware" + smithyHTTP "github.com/aws/smithy-go/transport/http" +) + +// HTTPPresigner is an interface to a SigV4a signer that can sign create a +// presigned URL for a HTTP requests. +type HTTPPresigner interface { + PresignHTTP( + ctx context.Context, credentials Credentials, r *http.Request, + payloadHash string, service string, regionSet []string, signingTime time.Time, + optFns ...func(*SignerOptions), + ) (url string, signedHeader http.Header, err error) +} + +// PresignHTTPRequestMiddlewareOptions is the options for the PresignHTTPRequestMiddleware middleware. +type PresignHTTPRequestMiddlewareOptions struct { + CredentialsProvider CredentialsProvider + Presigner HTTPPresigner + LogSigning bool +} + +// PresignHTTPRequestMiddleware provides the Finalize middleware for creating a +// presigned URL for an HTTP request. +// +// Will short circuit the middleware stack and not forward onto the next +// Finalize handler. +type PresignHTTPRequestMiddleware struct { + credentialsProvider CredentialsProvider + presigner HTTPPresigner + logSigning bool +} + +// NewPresignHTTPRequestMiddleware returns a new PresignHTTPRequestMiddleware +// initialized with the presigner. +func NewPresignHTTPRequestMiddleware(options PresignHTTPRequestMiddlewareOptions) *PresignHTTPRequestMiddleware { + return &PresignHTTPRequestMiddleware{ + credentialsProvider: options.CredentialsProvider, + presigner: options.Presigner, + logSigning: options.LogSigning, + } +} + +// ID provides the middleware ID. +func (*PresignHTTPRequestMiddleware) ID() string { return "PresignHTTPRequest" } + +// HandleFinalize will take the provided input and create a presigned url for +// the http request using the SigV4 presign authentication scheme. +func (s *PresignHTTPRequestMiddleware) HandleFinalize( + ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler, +) ( + out middleware.FinalizeOutput, metadata middleware.Metadata, err error, +) { + req, ok := in.Request.(*smithyHTTP.Request) + if !ok { + return out, metadata, &SigningError{ + Err: fmt.Errorf("unexpected request middleware type %T", in.Request), + } + } + + httpReq := req.Build(ctx) + if !hasCredentialProvider(s.credentialsProvider) { + out.Result = &v4.PresignedHTTPRequest{ + URL: httpReq.URL.String(), + Method: httpReq.Method, + SignedHeader: http.Header{}, + } + + return out, metadata, nil + } + + signingName := awsmiddleware.GetSigningName(ctx) + signingRegion := awsmiddleware.GetSigningRegion(ctx) + payloadHash := v4.GetPayloadHash(ctx) + if len(payloadHash) == 0 { + return out, metadata, &SigningError{ + Err: fmt.Errorf("computed payload hash missing from context"), + } + } + + credentials, err := s.credentialsProvider.RetrievePrivateKey(ctx) + if err != nil { + return out, metadata, &SigningError{ + Err: fmt.Errorf("failed to retrieve credentials: %w", err), + } + } + + u, h, err := s.presigner.PresignHTTP(ctx, credentials, + httpReq, payloadHash, signingName, []string{signingRegion}, time.Now(), + func(o *SignerOptions) { + o.Logger = middleware.GetLogger(ctx) + o.LogSigning = s.logSigning + }) + if err != nil { + return out, metadata, &SigningError{ + Err: fmt.Errorf("failed to sign http request, %w", err), + } + } + + out.Result = &v4.PresignedHTTPRequest{ + URL: u, + Method: httpReq.Method, + SignedHeader: h, + } + + return out, metadata, nil +} diff --git a/api/auth/signer/v4asdk2/presign_middleware_test.go b/api/auth/signer/v4asdk2/presign_middleware_test.go new file mode 100644 index 0000000..ec83a81 --- /dev/null +++ b/api/auth/signer/v4asdk2/presign_middleware_test.go @@ -0,0 +1,222 @@ +package v4a + +import ( + "bytes" + "context" + "net/http" + "net/url" + "strings" + "testing" + "time" + + awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + "github.com/aws/smithy-go/logging" + "github.com/aws/smithy-go/middleware" + smithyhttp "github.com/aws/smithy-go/transport/http" +) + +type httpPresignerFunc func( + ctx context.Context, credentials Credentials, r *http.Request, + payloadHash string, service string, regionSet []string, signingTime time.Time, + optFns ...func(*SignerOptions), +) (url string, signedHeader http.Header, err error) + +func (f httpPresignerFunc) PresignHTTP( + ctx context.Context, credentials Credentials, r *http.Request, + payloadHash string, service string, regionSet []string, signingTime time.Time, + optFns ...func(*SignerOptions), +) ( + url string, signedHeader http.Header, err error, +) { + return f(ctx, credentials, r, payloadHash, service, regionSet, signingTime, optFns...) +} + +func TestPresignHTTPRequestMiddleware(t *testing.T) { + cases := map[string]struct { + Request *http.Request + Creds CredentialsProvider + PayloadHash string + LogSigning bool + ExpectResult *v4.PresignedHTTPRequest + ExpectErr string + }{ + "success": { + Request: &http.Request{ + URL: func() *url.URL { + u, _ := url.Parse("https://example.aws/path?query=foo") + return u + }(), + Header: http.Header{}, + }, + Creds: stubCredentials, + PayloadHash: "0123456789abcdef", + ExpectResult: &v4.PresignedHTTPRequest{ + URL: "https://example.aws/path?query=foo", + SignedHeader: http.Header{}, + }, + }, + "error": { + Request: func() *http.Request { + return &http.Request{} + }(), + Creds: stubCredentials, + PayloadHash: "", + ExpectErr: "failed to sign request", + }, + "anonymous creds": { + Request: &http.Request{ + URL: func() *url.URL { + u, _ := url.Parse("https://example.aws/path?query=foo") + return u + }(), + Header: http.Header{}, + }, + Creds: stubCredentials, + PayloadHash: "", + ExpectErr: "failed to sign request", + ExpectResult: &v4.PresignedHTTPRequest{ + URL: "https://example.aws/path?query=foo", + SignedHeader: http.Header{}, + }, + }, + "nil creds": { + Request: &http.Request{ + URL: func() *url.URL { + u, _ := url.Parse("https://example.aws/path?query=foo") + return u + }(), + Header: http.Header{}, + }, + Creds: nil, + ExpectResult: &v4.PresignedHTTPRequest{ + URL: "https://example.aws/path?query=foo", + SignedHeader: http.Header{}, + }, + }, + "with log signing": { + Request: &http.Request{ + URL: func() *url.URL { + u, _ := url.Parse("https://example.aws/path?query=foo") + return u + }(), + Header: http.Header{}, + }, + Creds: stubCredentials, + PayloadHash: "0123456789abcdef", + ExpectResult: &v4.PresignedHTTPRequest{ + URL: "https://example.aws/path?query=foo", + SignedHeader: http.Header{}, + }, + + LogSigning: true, + }, + } + + const ( + signingName = "serviceId" + signingRegion = "regionName" + ) + + for name, tt := range cases { + t.Run(name, func(t *testing.T) { + m := &PresignHTTPRequestMiddleware{ + credentialsProvider: tt.Creds, + + presigner: httpPresignerFunc(func( + ctx context.Context, credentials Credentials, r *http.Request, + payloadHash string, service string, regionSet []string, signingTime time.Time, + optFns ...func(*SignerOptions), + ) (url string, signedHeader http.Header, err error) { + var options SignerOptions + for _, fn := range optFns { + fn(&options) + } + if options.Logger == nil { + t.Errorf("expect logger, got none") + } + if options.LogSigning { + options.Logger.Logf(logging.Debug, t.Name()) + } + + if !hasCredentialProvider(tt.Creds) { + t.Errorf("expect presigner not to be called for not credentials provider") + } + + expectCreds, _ := tt.Creds.RetrievePrivateKey(context.Background()) + if diff := cmpDiff(expectCreds, credentials); len(diff) > 0 { + t.Error(diff) + } + if e, a := tt.PayloadHash, payloadHash; e != a { + t.Errorf("expected %v, got %v", e, a) + } + if e, a := signingName, service; e != a { + t.Errorf("expected %v, got %v", e, a) + } + if diff := cmpDiff([]string{signingRegion}, regionSet); len(diff) > 0 { + t.Error(diff) + } + + return tt.ExpectResult.URL, tt.ExpectResult.SignedHeader, nil + }), + logSigning: tt.LogSigning, + } + + next := middleware.FinalizeHandlerFunc( + func(ctx context.Context, in middleware.FinalizeInput) ( + out middleware.FinalizeOutput, metadata middleware.Metadata, err error, + ) { + t.Errorf("expect next handler not to be called") + return out, metadata, err + }) + + ctx := awsmiddleware.SetSigningRegion( + awsmiddleware.SetSigningName(context.Background(), signingName), + signingRegion) + + var loggerBuf bytes.Buffer + logger := logging.NewStandardLogger(&loggerBuf) + ctx = middleware.SetLogger(ctx, logger) + + if len(tt.PayloadHash) != 0 { + ctx = v4.SetPayloadHash(ctx, tt.PayloadHash) + } + + result, _, err := m.HandleFinalize(ctx, middleware.FinalizeInput{ + Request: &smithyhttp.Request{ + Request: tt.Request, + }, + }, next) + if len(tt.ExpectErr) != 0 { + if err == nil { + t.Fatalf("expect error, got none") + } + if e, a := tt.ExpectErr, err.Error(); !strings.Contains(a, e) { + t.Fatalf("expect error to contain %v, got %v", e, a) + } + return + } + if err != nil { + t.Fatalf("expect no error, got %v", err) + } + + if diff := cmpDiff(tt.ExpectResult, result.Result); len(diff) != 0 { + t.Errorf("expect result match\n%v", diff) + } + + if tt.LogSigning { + if e, a := t.Name(), loggerBuf.String(); !strings.Contains(a, e) { + t.Errorf("expect %v logged in %v", e, a) + } + } else { + if loggerBuf.Len() != 0 { + t.Errorf("expect no log, got %v", loggerBuf.String()) + } + } + }) + } +} + +var ( + _ middleware.FinalizeMiddleware = &PresignHTTPRequestMiddleware{} +) diff --git a/api/auth/signer/v4asdk2/shared_test.go b/api/auth/signer/v4asdk2/shared_test.go new file mode 100644 index 0000000..6c94400 --- /dev/null +++ b/api/auth/signer/v4asdk2/shared_test.go @@ -0,0 +1,18 @@ +package v4a + +import ( + "bytes" + "context" + "crypto/ecdsa" +) + +var stubCredentials = stubCredentialsProviderFunc(func(ctx context.Context) (Credentials, error) { + stubKey, err := ecdsa.GenerateKey(p256, bytes.NewReader(bytes.Repeat([]byte{1}, 40))) + if err != nil { + return Credentials{}, err + } + return Credentials{ + Context: "STUB", + PrivateKey: stubKey, + }, nil +}) diff --git a/api/auth/signer/v4asdk2/smithy.go b/api/auth/signer/v4asdk2/smithy.go new file mode 100644 index 0000000..4ee4ca4 --- /dev/null +++ b/api/auth/signer/v4asdk2/smithy.go @@ -0,0 +1,85 @@ +package v4a + +import ( + "context" + "fmt" + "time" + + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + "github.com/aws/smithy-go" + "github.com/aws/smithy-go/auth" + "github.com/aws/smithy-go/logging" + smithyhttp "github.com/aws/smithy-go/transport/http" +) + +// CredentialsAdapter adapts v4a.Credentials to smithy auth.Identity. +type CredentialsAdapter struct { + Credentials Credentials +} + +var _ auth.Identity = (*CredentialsAdapter)(nil) + +// Expiration returns the time of expiration for the credentials. +func (v *CredentialsAdapter) Expiration() time.Time { + return v.Credentials.Expires +} + +// CredentialsProviderAdapter adapts v4a.CredentialsProvider to +// auth.IdentityResolver. +type CredentialsProviderAdapter struct { + Provider CredentialsProvider +} + +var _ (auth.IdentityResolver) = (*CredentialsProviderAdapter)(nil) + +// GetIdentity retrieves v4a credentials using the underlying provider. +func (v *CredentialsProviderAdapter) GetIdentity(ctx context.Context, _ smithy.Properties) ( + auth.Identity, error, +) { + creds, err := v.Provider.RetrievePrivateKey(ctx) + if err != nil { + return nil, fmt.Errorf("get credentials: %w", err) + } + + return &CredentialsAdapter{Credentials: creds}, nil +} + +// SignerAdapter adapts v4a.HTTPSigner to smithy http.Signer. +type SignerAdapter struct { + Signer HTTPSigner + Logger logging.Logger + LogSigning bool +} + +var _ (smithyhttp.Signer) = (*SignerAdapter)(nil) + +// SignRequest signs the request with the provided identity. +func (v *SignerAdapter) SignRequest(ctx context.Context, r *smithyhttp.Request, identity auth.Identity, props smithy.Properties) error { + ca, ok := identity.(*CredentialsAdapter) + if !ok { + return fmt.Errorf("unexpected identity type: %T", identity) + } + + name, ok := smithyhttp.GetSigV4SigningName(&props) + if !ok { + return fmt.Errorf("sigv4a signing name is required") + } + + regions, ok := smithyhttp.GetSigV4ASigningRegions(&props) + if !ok { + return fmt.Errorf("sigv4a signing region is required") + } + + hash := v4.GetPayloadHash(ctx) + err := v.Signer.SignHTTP(ctx, ca.Credentials, r.Request, hash, name, regions, time.Now(), func(o *SignerOptions) { + o.DisableURIPathEscaping, _ = smithyhttp.GetDisableDoubleEncoding(&props) + + o.Logger = v.Logger + o.LogSigning = v.LogSigning + }) + if err != nil { + return fmt.Errorf("sign http: %w", err) + } + + return nil +} diff --git a/api/auth/signer/v4asdk2/stream.go b/api/auth/signer/v4asdk2/stream.go new file mode 100644 index 0000000..b168c6c --- /dev/null +++ b/api/auth/signer/v4asdk2/stream.go @@ -0,0 +1,96 @@ +package v4a + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" + "time" + + signerCrypto "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2/internal/crypto" + v4Internal "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2/internal/v4" +) + +// EventStreamSigner is an AWS EventStream protocol signer. +type EventStreamSigner interface { + GetSignature(ctx context.Context, headers, payload []byte, signingTime time.Time, optFns ...func(*StreamSignerOptions)) ([]byte, error) +} + +// StreamSignerOptions is the configuration options for StreamSigner. +type StreamSignerOptions struct{} + +// StreamSigner implements Signature Version 4 (SigV4) signing of event stream encoded payloads. +type StreamSigner struct { + options StreamSignerOptions + + credentials Credentials + service string + + prevSignature []byte +} + +// NewStreamSigner returns a new AWS EventStream protocol signer. +func NewStreamSigner(credentials Credentials, service string, seedSignature []byte, optFns ...func(*StreamSignerOptions)) *StreamSigner { + o := StreamSignerOptions{} + + for _, fn := range optFns { + fn(&o) + } + + return &StreamSigner{ + options: o, + credentials: credentials, + service: service, + prevSignature: seedSignature, + } +} + +func (s *StreamSigner) VerifySignature(headers, payload []byte, signingTime time.Time, signature []byte, optFns ...func(*StreamSignerOptions)) error { + options := s.options + + for _, fn := range optFns { + fn(&options) + } + + prevSignature := s.prevSignature + + st := v4Internal.NewSigningTime(signingTime) + + scope := buildCredentialScope(st, s.service) + + stringToSign := s.buildEventStreamStringToSign(headers, payload, prevSignature, scope, &st) + + ok, err := signerCrypto.VerifySignature(&s.credentials.PrivateKey.PublicKey, makeHash(sha256.New(), []byte(stringToSign)), signature) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("v4a: invalid signature") + } + + s.prevSignature = signature + + return nil +} + +func (s *StreamSigner) buildEventStreamStringToSign(headers, payload, previousSignature []byte, credentialScope string, signingTime *v4Internal.SigningTime) string { + hash := sha256.New() + return strings.Join([]string{ + "AWS4-ECDSA-P256-SHA256-PAYLOAD", + signingTime.TimeFormat(), + credentialScope, + hex.EncodeToString(previousSignature), + hex.EncodeToString(makeHash(hash, headers)), + hex.EncodeToString(makeHash(hash, payload)), + }, "\n") +} + +func buildCredentialScope(st v4Internal.SigningTime, service string) string { + return strings.Join([]string{ + st.Format(shortTimeFormat), + service, + "aws4_request", + }, "/") + +} diff --git a/api/auth/signer/v4asdk2/v4a.go b/api/auth/signer/v4asdk2/v4a.go new file mode 100644 index 0000000..305fbd1 --- /dev/null +++ b/api/auth/signer/v4asdk2/v4a.go @@ -0,0 +1,577 @@ +package v4a + +import ( + "bytes" + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "hash" + "math/big" + "net/http" + "net/textproto" + "net/url" + "sort" + "strconv" + "strings" + "time" + + signerCrypto "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2/internal/crypto" + v4Internal "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2/internal/v4" + "github.com/aws/smithy-go/encoding/httpbinding" + "github.com/aws/smithy-go/logging" +) + +const ( + // AmzRegionSetKey represents the region set header used for sigv4a + AmzRegionSetKey = "X-Amz-Region-Set" + amzAlgorithmKey = v4Internal.AmzAlgorithmKey + amzSecurityTokenKey = v4Internal.AmzSecurityTokenKey + amzDateKey = v4Internal.AmzDateKey + amzCredentialKey = v4Internal.AmzCredentialKey + amzSignedHeadersKey = v4Internal.AmzSignedHeadersKey + authorizationHeader = "Authorization" + + signingAlgorithm = "AWS4-ECDSA-P256-SHA256" + + timeFormat = "20060102T150405Z" + shortTimeFormat = "20060102" + + // EmptyStringSHA256 is a hex encoded SHA-256 hash of an empty string + EmptyStringSHA256 = v4Internal.EmptyStringSHA256 + + // Version of signing v4a + Version = "SigV4A" +) + +var ( + p256 elliptic.Curve + nMinusTwoP256 *big.Int + + one = new(big.Int).SetInt64(1) +) + +func init() { + // Ensure the elliptic curve parameters are initialized on package import rather then on first usage + p256 = elliptic.P256() + + nMinusTwoP256 = new(big.Int).SetBytes(p256.Params().N.Bytes()) + nMinusTwoP256 = nMinusTwoP256.Sub(nMinusTwoP256, new(big.Int).SetInt64(2)) +} + +// SignerOptions is the SigV4a signing options for constructing a Signer. +type SignerOptions struct { + Logger logging.Logger + LogSigning bool + + // Disables the Signer's moving HTTP header key/value pairs from the HTTP + // request header to the request's query string. This is most commonly used + // with pre-signed requests preventing headers from being added to the + // request's query string. + DisableHeaderHoisting bool + + // Disables the automatic escaping of the URI path of the request for the + // siganture's canonical string's path. For services that do not need additional + // escaping then use this to disable the signer escaping the path. + // + // S3 is an example of a service that does not need additional escaping. + // + // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html + DisableURIPathEscaping bool +} + +// Signer is a SigV4a HTTP signing implementation +type Signer struct { + options SignerOptions +} + +// NewSigner constructs a SigV4a Signer. +func NewSigner(optFns ...func(*SignerOptions)) *Signer { + options := SignerOptions{} + + for _, fn := range optFns { + fn(&options) + } + + return &Signer{options: options} +} + +// deriveKeyFromAccessKeyPair derives a NIST P-256 PrivateKey from the given +// IAM AccessKey and SecretKey pair. +// +// Based on FIPS.186-4 Appendix B.4.2 +func deriveKeyFromAccessKeyPair(accessKey, secretKey string) (*ecdsa.PrivateKey, error) { + params := p256.Params() + bitLen := params.BitSize // Testing random candidates does not require an additional 64 bits + counter := 0x01 + + buffer := make([]byte, 1+len(accessKey)) // 1 byte counter + len(accessKey) + kdfContext := bytes.NewBuffer(buffer) + + inputKey := append([]byte("AWS4A"), []byte(secretKey)...) + + d := new(big.Int) + for { + kdfContext.Reset() + kdfContext.WriteString(accessKey) + kdfContext.WriteByte(byte(counter)) + + key, err := signerCrypto.HMACKeyDerivation(sha256.New, bitLen, inputKey, []byte(signingAlgorithm), kdfContext.Bytes()) + if err != nil { + return nil, err + } + + // Check key first before calling SetBytes if key key is in fact a valid candidate. + // This ensures the byte slice is the correct length (32-bytes) to compare in constant-time + cmp, err := signerCrypto.ConstantTimeByteCompare(key, nMinusTwoP256.Bytes()) + if err != nil { + return nil, err + } + if cmp == -1 { + d.SetBytes(key) + break + } + + counter++ + if counter > 0xFF { + return nil, fmt.Errorf("exhausted single byte external counter") + } + } + d = d.Add(d, one) + + priv := new(ecdsa.PrivateKey) + priv.PublicKey.Curve = p256 + priv.D = d + priv.PublicKey.X, priv.PublicKey.Y = p256.ScalarBaseMult(d.Bytes()) + + return priv, nil +} + +type httpSigner struct { + Request *http.Request + ServiceName string + RegionSet []string + Time time.Time + Credentials Credentials + IsPreSign bool + + Logger logging.Logger + Debug bool + + // PayloadHash is the hex encoded SHA-256 hash of the request payload + // If len(PayloadHash) == 0 the signer will attempt to send the request + // as an unsigned payload. Note: Unsigned payloads only work for a subset of services. + PayloadHash string + + DisableHeaderHoisting bool + DisableURIPathEscaping bool +} + +// SignHTTP takes the provided http.Request, payload hash, service, regionSet, and time and signs using SigV4a. +// The passed in request will be modified in place. +func (s *Signer) SignHTTP(ctx context.Context, credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, optFns ...func(*SignerOptions)) error { + options := s.options + for _, fn := range optFns { + fn(&options) + } + + signer := &httpSigner{ + Request: r, + PayloadHash: payloadHash, + ServiceName: service, + RegionSet: regionSet, + Credentials: credentials, + Time: signingTime.UTC(), + DisableHeaderHoisting: options.DisableHeaderHoisting, + DisableURIPathEscaping: options.DisableURIPathEscaping, + } + + signedRequest, err := signer.Build() + if err != nil { + return err + } + + logHTTPSigningInfo(ctx, options, signedRequest) + + return nil +} + +// 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 { + options := s.options + for _, fn := range optFns { + fn(&options) + } + + signer := &httpSigner{ + Request: r, + PayloadHash: payloadHash, + ServiceName: service, + RegionSet: regionSet, + Credentials: credentials, + Time: signingTime.UTC(), + DisableHeaderHoisting: options.DisableHeaderHoisting, + DisableURIPathEscaping: options.DisableURIPathEscaping, + } + + signedReq, err := signer.Build() + if err != nil { + return err + } + + logHTTPSigningInfo(context.TODO(), options, signedReq) + + signatureRaw, err := hex.DecodeString(signature) + if err != nil { + return fmt.Errorf("decode hex signature: %w", err) + } + + ok, err := signerCrypto.VerifySignature(&credentials.PrivateKey.PublicKey, makeHash(sha256.New(), []byte(signedReq.StringToSign)), signatureRaw) + if err != nil { + return err + } + + if !ok { + return fmt.Errorf("v4a: invalid signature") + } + + return nil +} + +// PresignHTTP takes the provided http.Request, payload hash, service, regionSet, and time and presigns using SigV4a +// Returns the presigned URL along with the headers that were signed with the request. +// +// PresignHTTP will not set the expires time of the presigned request +// automatically. To specify the expire duration for a request add the +// "X-Amz-Expires" query parameter on the request with the value as the +// duration in seconds the presigned URL should be considered valid for. This +// parameter is not used by all AWS services, and is most notable used by +// Amazon S3 APIs. +func (s *Signer) PresignHTTP(ctx context.Context, credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, optFns ...func(*SignerOptions)) (signedURI string, signedHeaders http.Header, err error) { + options := s.options + for _, fn := range optFns { + fn(&options) + } + + signer := &httpSigner{ + Request: r, + PayloadHash: payloadHash, + ServiceName: service, + RegionSet: regionSet, + Credentials: credentials, + Time: signingTime.UTC(), + IsPreSign: true, + DisableHeaderHoisting: options.DisableHeaderHoisting, + DisableURIPathEscaping: options.DisableURIPathEscaping, + } + + signedRequest, err := signer.Build() + if err != nil { + return "", nil, err + } + + logHTTPSigningInfo(ctx, options, signedRequest) + + signedHeaders = make(http.Header) + + // For the signed headers we canonicalize the header keys in the returned map. + // This avoids situations where can standard library double headers like host header. For example the standard + // library will set the Host header, even if it is present in lower-case form. + for k, v := range signedRequest.SignedHeaders { + key := textproto.CanonicalMIMEHeaderKey(k) + signedHeaders[key] = append(signedHeaders[key], v...) + } + + return signedRequest.Request.URL.String(), signedHeaders, nil +} + +func (s *httpSigner) setRequiredSigningFields(headers http.Header, query url.Values) { + amzDate := s.Time.Format(timeFormat) + + if s.IsPreSign { + query.Set(AmzRegionSetKey, strings.Join(s.RegionSet, ",")) + query.Set(amzDateKey, amzDate) + query.Set(amzAlgorithmKey, signingAlgorithm) + if len(s.Credentials.SessionToken) > 0 { + query.Set(amzSecurityTokenKey, s.Credentials.SessionToken) + } + return + } + + headers.Set(AmzRegionSetKey, strings.Join(s.RegionSet, ",")) + headers.Set(amzDateKey, amzDate) + if len(s.Credentials.SessionToken) > 0 { + headers.Set(amzSecurityTokenKey, s.Credentials.SessionToken) + } +} + +func (s *httpSigner) Build() (signedRequest, error) { + req := s.Request + + query := req.URL.Query() + headers := req.Header + + s.setRequiredSigningFields(headers, query) + + // Sort Each Query Key's Values + for key := range query { + sort.Strings(query[key]) + } + + v4Internal.SanitizeHostForHeader(req) + + credentialScope := s.buildCredentialScope() + credentialStr := s.Credentials.Context + "/" + credentialScope + if s.IsPreSign { + query.Set(amzCredentialKey, credentialStr) + } + + unsignedHeaders := headers + if s.IsPreSign && !s.DisableHeaderHoisting { + urlValues := url.Values{} + urlValues, unsignedHeaders = buildQuery(v4Internal.AllowedQueryHoisting, unsignedHeaders) + for k := range urlValues { + query[k] = urlValues[k] + } + } + + host := req.URL.Host + if len(req.Host) > 0 { + host = req.Host + } + + var ( + signedHeaders http.Header + signedHeadersStr string + canonicalHeaderStr string + ) + + if s.IsPreSign { + signedHeaders, signedHeadersStr, canonicalHeaderStr = s.buildCanonicalHeaders(host, v4Internal.IgnoredPresignedHeaders, unsignedHeaders, s.Request.ContentLength) + } else { + signedHeaders, signedHeadersStr, canonicalHeaderStr = s.buildCanonicalHeaders(host, v4Internal.IgnoredHeaders, unsignedHeaders, s.Request.ContentLength) + } + + if s.IsPreSign { + query.Set(amzSignedHeadersKey, signedHeadersStr) + } + + rawQuery := strings.Replace(query.Encode(), "+", "%20", -1) + + canonicalURI := v4Internal.GetURIPath(req.URL) + if !s.DisableURIPathEscaping { + canonicalURI = httpbinding.EscapePath(canonicalURI, false) + } + + canonicalString := s.buildCanonicalString( + req.Method, + canonicalURI, + rawQuery, + signedHeadersStr, + canonicalHeaderStr, + ) + + strToSign := s.buildStringToSign(credentialScope, canonicalString) + signingSignature, err := s.buildSignature(strToSign) + if err != nil { + return signedRequest{}, err + } + + if s.IsPreSign { + rawQuery += "&X-Amz-Signature=" + signingSignature + } else { + headers[authorizationHeader] = append(headers[authorizationHeader][:0], buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature)) + } + + req.URL.RawQuery = rawQuery + + return signedRequest{ + Request: req, + SignedHeaders: signedHeaders, + CanonicalString: canonicalString, + StringToSign: strToSign, + PreSigned: s.IsPreSign, + }, nil +} + +func buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature string) string { + const credential = "Credential=" + const signedHeaders = "SignedHeaders=" + const signature = "Signature=" + const commaSpace = ", " + + var parts strings.Builder + parts.Grow(len(signingAlgorithm) + 1 + + len(credential) + len(credentialStr) + len(commaSpace) + + len(signedHeaders) + len(signedHeadersStr) + len(commaSpace) + + len(signature) + len(signingSignature), + ) + parts.WriteString(signingAlgorithm) + parts.WriteRune(' ') + parts.WriteString(credential) + parts.WriteString(credentialStr) + parts.WriteString(commaSpace) + parts.WriteString(signedHeaders) + parts.WriteString(signedHeadersStr) + parts.WriteString(commaSpace) + parts.WriteString(signature) + parts.WriteString(signingSignature) + return parts.String() +} + +func (s *httpSigner) buildCredentialScope() string { + return strings.Join([]string{ + s.Time.Format(shortTimeFormat), + s.ServiceName, + "aws4_request", + }, "/") + +} + +func buildQuery(r v4Internal.Rule, header http.Header) (url.Values, http.Header) { + query := url.Values{} + unsignedHeaders := http.Header{} + for k, h := range header { + if r.IsValid(k) { + query[k] = h + } else { + unsignedHeaders[k] = h + } + } + + return query, unsignedHeaders +} + +func (s *httpSigner) buildCanonicalHeaders(host string, rule v4Internal.Rule, header http.Header, length int64) (signed http.Header, signedHeaders, canonicalHeadersStr string) { + signed = make(http.Header) + + var headers []string + const hostHeader = "host" + headers = append(headers, hostHeader) + signed[hostHeader] = append(signed[hostHeader], host) + + const contentLengthHeader = "content-length" + if length > 0 { + headers = append(headers, contentLengthHeader) + signed[contentLengthHeader] = append(signed[contentLengthHeader], strconv.FormatInt(length, 10)) + } + + for k, v := range header { + if !rule.IsValid(k) { + continue // ignored header + } + + if strings.EqualFold(k, contentLengthHeader) { + // prevent signing already handled content-length header. + continue + } + + lowerCaseKey := strings.ToLower(k) + if _, ok := signed[lowerCaseKey]; ok { + // include additional values + signed[lowerCaseKey] = append(signed[lowerCaseKey], v...) + continue + } + + headers = append(headers, lowerCaseKey) + signed[lowerCaseKey] = v + } + sort.Strings(headers) + + signedHeaders = strings.Join(headers, ";") + + var canonicalHeaders strings.Builder + n := len(headers) + const colon = ':' + for i := 0; i < n; i++ { + if headers[i] == hostHeader { + canonicalHeaders.WriteString(hostHeader) + canonicalHeaders.WriteRune(colon) + canonicalHeaders.WriteString(v4Internal.StripExcessSpaces(host)) + } else { + canonicalHeaders.WriteString(headers[i]) + canonicalHeaders.WriteRune(colon) + // Trim out leading, trailing, and dedup inner spaces from signed header values. + values := signed[headers[i]] + for j, v := range values { + cleanedValue := strings.TrimSpace(v4Internal.StripExcessSpaces(v)) + canonicalHeaders.WriteString(cleanedValue) + if j < len(values)-1 { + canonicalHeaders.WriteRune(',') + } + } + } + canonicalHeaders.WriteRune('\n') + } + canonicalHeadersStr = canonicalHeaders.String() + + return signed, signedHeaders, canonicalHeadersStr +} + +func (s *httpSigner) buildCanonicalString(method, uri, query, signedHeaders, canonicalHeaders string) string { + return strings.Join([]string{ + method, + uri, + query, + canonicalHeaders, + signedHeaders, + s.PayloadHash, + }, "\n") +} + +func (s *httpSigner) buildStringToSign(credentialScope, canonicalRequestString string) string { + return strings.Join([]string{ + signingAlgorithm, + s.Time.Format(timeFormat), + credentialScope, + hex.EncodeToString(makeHash(sha256.New(), []byte(canonicalRequestString))), + }, "\n") +} + +func makeHash(hash hash.Hash, b []byte) []byte { + hash.Reset() + hash.Write(b) + return hash.Sum(nil) +} + +func (s *httpSigner) buildSignature(strToSign string) (string, error) { + sig, err := s.Credentials.PrivateKey.Sign(rand.Reader, makeHash(sha256.New(), []byte(strToSign)), crypto.SHA256) + if err != nil { + return "", err + } + return hex.EncodeToString(sig), nil +} + +const logSignInfoMsg = `Request Signature: +---[ CANONICAL STRING ]----------------------------- +%s +---[ STRING TO SIGN ]-------------------------------- +%s%s +-----------------------------------------------------` +const logSignedURLMsg = ` +---[ SIGNED URL ]------------------------------------ +%s` + +func logHTTPSigningInfo(ctx context.Context, options SignerOptions, r signedRequest) { + if !options.LogSigning { + return + } + signedURLMsg := "" + if r.PreSigned { + signedURLMsg = fmt.Sprintf(logSignedURLMsg, r.Request.URL.String()) + } + logger := logging.WithContext(ctx, options.Logger) + logger.Logf(logging.Debug, logSignInfoMsg, r.CanonicalString, r.StringToSign, signedURLMsg) +} + +type signedRequest struct { + Request *http.Request + SignedHeaders http.Header + CanonicalString string + StringToSign string + PreSigned bool +} diff --git a/api/auth/signer/v4asdk2/v4a_test.go b/api/auth/signer/v4asdk2/v4a_test.go new file mode 100644 index 0000000..a67ca20 --- /dev/null +++ b/api/auth/signer/v4asdk2/v4a_test.go @@ -0,0 +1,429 @@ +package v4a + +import ( + "context" + "encoding/hex" + "fmt" + "math/big" + "net/http" + "net/url" + "reflect" + "strconv" + "strings" + "testing" + "time" + + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2/internal/crypto" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/smithy-go/logging" +) + +const ( + accessKey = "AKISORANDOMAASORANDOM" + secretKey = "q+jcrXGc+0zWN6uzclKVhvMmUsIfRPa4rlRandom" +) + +func TestDeriveECDSAKeyPairFromSecret(t *testing.T) { + privateKey, err := deriveKeyFromAccessKeyPair(accessKey, secretKey) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expectedX := func() *big.Int { + t.Helper() + b, ok := new(big.Int).SetString("15D242CEEBF8D8169FD6A8B5A746C41140414C3B07579038DA06AF89190FFFCB", 16) + if !ok { + t.Fatalf("failed to parse big integer") + } + return b + }() + expectedY := func() *big.Int { + t.Helper() + b, ok := new(big.Int).SetString("515242CEDD82E94799482E4C0514B505AFCCF2C0C98D6A553BF539F424C5EC0", 16) + if !ok { + t.Fatalf("failed to parse big integer") + } + return b + }() + + if privateKey.X.Cmp(expectedX) != 0 { + t.Errorf("expected % X, got % X", expectedX, privateKey.X) + } + if privateKey.Y.Cmp(expectedY) != 0 { + t.Errorf("expected % X, got % X", expectedY, privateKey.Y) + } +} + +func TestSignHTTP(t *testing.T) { + req := buildRequest("dynamodb", "us-east-1") + + signer, credProvider := buildSigner(t, true) + + key, err := credProvider.RetrievePrivateKey(context.Background()) + if err != nil { + t.Fatalf("expect no error, got %v", err) + } + + err = signer.SignHTTP(context.Background(), key, req, EmptyStringSHA256, "dynamodb", []string{"us-east-1"}, time.Unix(0, 0)) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expectedDate := "19700101T000000Z" + expectedAlg := "AWS4-ECDSA-P256-SHA256" + expectedCredential := "AKISORANDOMAASORANDOM/19700101/dynamodb/aws4_request" + expectedSignedHeaders := "content-length;content-type;host;x-amz-date;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore;x-amz-region-set;x-amz-security-token;x-amz-target" + expectedStrToSignHash := "4ba7d0482cf4d5450cefdc067a00de1a4a715e444856fa3e1d85c35fb34d9730" + + q := req.Header + + validateAuthorization(t, q.Get("Authorization"), expectedAlg, expectedCredential, expectedSignedHeaders, expectedStrToSignHash) + + if e, a := expectedDate, q.Get("X-Amz-Date"); e != a { + t.Errorf("expect %v, got %v", e, a) + } +} + +func TestSignHTTP_NoSessionToken(t *testing.T) { + req := buildRequest("dynamodb", "us-east-1") + + signer, credProvider := buildSigner(t, false) + + key, err := credProvider.RetrievePrivateKey(context.Background()) + if err != nil { + t.Fatalf("expect no error, got %v", err) + } + + err = signer.SignHTTP(context.Background(), key, req, EmptyStringSHA256, "dynamodb", []string{"us-east-1"}, time.Unix(0, 0)) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expectedAlg := "AWS4-ECDSA-P256-SHA256" + expectedCredential := "AKISORANDOMAASORANDOM/19700101/dynamodb/aws4_request" + expectedSignedHeaders := "content-length;content-type;host;x-amz-date;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore;x-amz-region-set;x-amz-target" + expectedStrToSignHash := "1aeefb422ae6aa0de7aec829da813e55cff35553cac212dffd5f9474c71e47ee" + + q := req.Header + + validateAuthorization(t, q.Get("Authorization"), expectedAlg, expectedCredential, expectedSignedHeaders, expectedStrToSignHash) +} + +func TestPresignHTTP(t *testing.T) { + req := buildRequest("dynamodb", "us-east-1") + + signer, credProvider := buildSigner(t, false) + + key, err := credProvider.RetrievePrivateKey(context.Background()) + if err != nil { + t.Fatalf("expect no error, got %v", err) + } + + query := req.URL.Query() + query.Set("X-Amz-Expires", "18000") + req.URL.RawQuery = query.Encode() + + signedURL, _, err := signer.PresignHTTP(context.Background(), key, req, EmptyStringSHA256, "dynamodb", []string{"us-east-1"}, time.Unix(0, 0)) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expectedDate := "19700101T000000Z" + expectedAlg := "AWS4-ECDSA-P256-SHA256" + expectedHeaders := "content-length;content-type;host;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore" + expectedCredential := "AKISORANDOMAASORANDOM/19700101/dynamodb/aws4_request" + expectedStrToSignHash := "d7ffbd2fab644384c056957e6ac38de4ae68246764b5f5df171b3824153b6397" + expectedTarget := "prefix.Operation" + + signedReq, err := url.Parse(signedURL) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + q := signedReq.Query() + + validateSignature(t, expectedStrToSignHash, q.Get("X-Amz-Signature")) + + if e, a := expectedAlg, q.Get("X-Amz-Algorithm"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := expectedCredential, q.Get("X-Amz-Credential"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := expectedHeaders, q.Get("X-Amz-SignedHeaders"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := expectedDate, q.Get("X-Amz-Date"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if a := q.Get("X-Amz-Meta-Other-Header"); len(a) != 0 { + t.Errorf("expect %v to be empty", a) + } + if e, a := expectedTarget, q.Get("X-Amz-Target"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := "us-east-1", q.Get("X-Amz-Region-Set"); e != a { + t.Errorf("expect %v, got %v", e, a) + } +} + +func TestPresignHTTP_BodyWithArrayRequest(t *testing.T) { + req := buildRequest("dynamodb", "us-east-1") + req.URL.RawQuery = "Foo=z&Foo=o&Foo=m&Foo=a" + + signer, credProvider := buildSigner(t, true) + + key, err := credProvider.RetrievePrivateKey(context.Background()) + if err != nil { + t.Fatalf("expect no error, got %v", err) + } + + query := req.URL.Query() + query.Set("X-Amz-Expires", "300") + req.URL.RawQuery = query.Encode() + + signedURI, _, err := signer.PresignHTTP(context.Background(), key, req, EmptyStringSHA256, "dynamodb", []string{"us-east-1"}, time.Unix(0, 0)) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + signedReq, err := url.Parse(signedURI) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expectedAlg := "AWS4-ECDSA-P256-SHA256" + expectedDate := "19700101T000000Z" + expectedHeaders := "content-length;content-type;host;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore" + expectedStrToSignHash := "acff64fd3689be96259d4112c3742ff79f4da0d813bc58a285dc1c4449760bec" + expectedCred := "AKISORANDOMAASORANDOM/19700101/dynamodb/aws4_request" + expectedTarget := "prefix.Operation" + + q := signedReq.Query() + + validateSignature(t, expectedStrToSignHash, q.Get("X-Amz-Signature")) + + if e, a := expectedAlg, q.Get("X-Amz-Algorithm"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := expectedCred, q.Get("X-Amz-Credential"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := expectedHeaders, q.Get("X-Amz-SignedHeaders"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := expectedDate, q.Get("X-Amz-Date"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if a := q.Get("X-Amz-Meta-Other-Header"); len(a) != 0 { + t.Errorf("expect %v to be empty, was not", a) + } + if e, a := expectedTarget, q.Get("X-Amz-Target"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := "us-east-1", q.Get("X-Amz-Region-Set"); e != a { + t.Errorf("expect %v, got %v", e, a) + } +} +func TestSign_buildCanonicalHeaders(t *testing.T) { + serviceName := "mockAPI" + region := "mock-region" + endpoint := "https://" + serviceName + "." + region + ".amazonaws.com" + + req, err := http.NewRequest("POST", endpoint, nil) + if err != nil { + t.Fatalf("failed to create request, %v", err) + } + + req.Header.Set("FooInnerSpace", " inner space ") + req.Header.Set("FooLeadingSpace", " leading-space") + req.Header.Add("FooMultipleSpace", "no-space") + req.Header.Add("FooMultipleSpace", "\ttab-space") + req.Header.Add("FooMultipleSpace", "trailing-space ") + req.Header.Set("FooNoSpace", "no-space") + req.Header.Set("FooTabSpace", "\ttab-space\t") + req.Header.Set("FooTrailingSpace", "trailing-space ") + req.Header.Set("FooWrappedSpace", " wrapped-space ") + + credProvider := &SymmetricCredentialAdaptor{ + SymmetricProvider: staticCredentialsProvider{ + Value: aws.Credentials{ + AccessKeyID: accessKey, + SecretAccessKey: secretKey, + }, + }, + } + key, err := credProvider.RetrievePrivateKey(context.Background()) + if err != nil { + t.Fatalf("expect no error, got %v", err) + } + + ctx := &httpSigner{ + Request: req, + ServiceName: serviceName, + RegionSet: []string{region}, + Credentials: key, + Time: time.Date(2021, 10, 20, 12, 42, 0, 0, time.UTC), + } + + build, err := ctx.Build() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expectCanonicalString := strings.Join([]string{ + `POST`, + `/`, + ``, + `fooinnerspace:inner space`, + `fooleadingspace:leading-space`, + `foomultiplespace:no-space,tab-space,trailing-space`, + `foonospace:no-space`, + `footabspace:tab-space`, + `footrailingspace:trailing-space`, + `foowrappedspace:wrapped-space`, + `host:mockAPI.mock-region.amazonaws.com`, + `x-amz-date:20211020T124200Z`, + `x-amz-region-set:mock-region`, + ``, + `fooinnerspace;fooleadingspace;foomultiplespace;foonospace;footabspace;footrailingspace;foowrappedspace;host;x-amz-date;x-amz-region-set`, + ``, + }, "\n") + if diff := cmpDiff(expectCanonicalString, build.CanonicalString); diff != "" { + t.Errorf("expect match, got\n%s", diff) + } +} + +func validateAuthorization(t *testing.T, authorization, expectedAlg, expectedCredential, expectedSignedHeaders, expectedStrToSignHash string) { + t.Helper() + split := strings.SplitN(authorization, " ", 2) + + if len(split) != 2 { + t.Fatal("unexpected authorization header format") + } + + if e, a := split[0], expectedAlg; e != a { + t.Errorf("expected %v, got %v", e, a) + } + + keyValues := strings.Split(split[1], ", ") + seen := make(map[string]string) + + for _, kv := range keyValues { + idx := strings.Index(kv, "=") + if idx == -1 { + continue + } + key, value := kv[:idx], kv[idx+1:] + seen[key] = value + } + + if a, ok := seen["Credential"]; ok { + if expectedCredential != a { + t.Errorf("expected credential %v, got %v", expectedCredential, a) + } + } else { + t.Errorf("Credential not found in authorization string") + } + + if a, ok := seen["SignedHeaders"]; ok { + if expectedSignedHeaders != a { + t.Errorf("expected signed headers %v, got %v", expectedSignedHeaders, a) + } + } else { + t.Errorf("SignedHeaders not found in authorization string") + } + + if a, ok := seen["Signature"]; ok { + validateSignature(t, expectedStrToSignHash, a) + } else { + t.Errorf("signature not found in authorization string") + } +} + +func validateSignature(t *testing.T, expectedHash, signature string) { + t.Helper() + pair, err := deriveKeyFromAccessKeyPair(accessKey, secretKey) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + hash, _ := hex.DecodeString(expectedHash) + sig, _ := hex.DecodeString(signature) + + ok, err := crypto.VerifySignature(&pair.PublicKey, hash, sig) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if !ok { + t.Errorf("failed to verify signing singature") + } +} + +func buildRequest(serviceName, region string) *http.Request { + endpoint := "https://" + serviceName + "." + region + ".amazonaws.com" + req, _ := http.NewRequest("POST", endpoint, nil) + req.URL.Opaque = "//example.org/bucket/key-._~,!@%23$%25^&*()" + req.Header.Set("X-Amz-Target", "prefix.Operation") + req.Header.Set("Content-Type", "application/x-amz-json-1.0") + + req.Header.Set("Content-Length", strconv.Itoa(1024)) + + req.Header.Set("X-Amz-Meta-Other-Header", "some-value=!@#$%^&* (+)") + req.Header.Add("X-Amz-Meta-Other-Header_With_Underscore", "some-value=!@#$%^&* (+)") + req.Header.Add("X-amz-Meta-Other-Header_With_Underscore", "some-value=!@#$%^&* (+)") + return req +} + +func buildSigner(t *testing.T, withToken bool) (*Signer, CredentialsProvider) { + creds := aws.Credentials{ + AccessKeyID: accessKey, + SecretAccessKey: secretKey, + } + + if withToken { + creds.SessionToken = "TOKEN" + } + + return NewSigner(func(options *SignerOptions) { + options.Logger = loggerFunc(func(format string, v ...interface{}) { + t.Logf(format, v...) + }) + }), &SymmetricCredentialAdaptor{ + SymmetricProvider: staticCredentialsProvider{ + Value: creds, + }, + } +} + +type loggerFunc func(format string, v ...interface{}) + +func (l loggerFunc) Logf(_ logging.Classification, format string, v ...interface{}) { + l(format, v...) +} + +type staticCredentialsProvider struct { + Value aws.Credentials +} + +func (s staticCredentialsProvider) Retrieve(_ context.Context) (aws.Credentials, error) { + v := s.Value + if v.AccessKeyID == "" || v.SecretAccessKey == "" { + return aws.Credentials{ + Source: "Source Name", + }, fmt.Errorf("static credentials are empty") + } + + if len(v.Source) == 0 { + v.Source = "Source Name" + } + + return v, nil +} + +func cmpDiff(e, a interface{}) string { + if !reflect.DeepEqual(e, a) { + return fmt.Sprintf("%v != %v", e, a) + } + return "" +} diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/cache.go b/api/auth/signer/v4sdk2/signer/internal/v4/cache.go new file mode 100644 index 0000000..cbf22f1 --- /dev/null +++ b/api/auth/signer/v4sdk2/signer/internal/v4/cache.go @@ -0,0 +1,115 @@ +package v4 + +import ( + "strings" + "sync" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" +) + +func lookupKey(service, region string) string { + var s strings.Builder + s.Grow(len(region) + len(service) + 3) + s.WriteString(region) + s.WriteRune('/') + s.WriteString(service) + return s.String() +} + +type derivedKey struct { + AccessKey string + Date time.Time + Credential []byte +} + +type derivedKeyCache struct { + values map[string]derivedKey + mutex sync.RWMutex +} + +func newDerivedKeyCache() derivedKeyCache { + return derivedKeyCache{ + values: make(map[string]derivedKey), + } +} + +func (s *derivedKeyCache) Get(credentials aws.Credentials, service, region string, signingTime SigningTime) []byte { + key := lookupKey(service, region) + s.mutex.RLock() + if cred, ok := s.get(key, credentials, signingTime.Time); ok { + s.mutex.RUnlock() + return cred + } + s.mutex.RUnlock() + + s.mutex.Lock() + if cred, ok := s.get(key, credentials, signingTime.Time); ok { + s.mutex.Unlock() + return cred + } + cred := deriveKey(credentials.SecretAccessKey, service, region, signingTime) + entry := derivedKey{ + AccessKey: credentials.AccessKeyID, + Date: signingTime.Time, + Credential: cred, + } + s.values[key] = entry + s.mutex.Unlock() + + return cred +} + +func (s *derivedKeyCache) get(key string, credentials aws.Credentials, signingTime time.Time) ([]byte, bool) { + cacheEntry, ok := s.retrieveFromCache(key) + if ok && cacheEntry.AccessKey == credentials.AccessKeyID && isSameDay(signingTime, cacheEntry.Date) { + return cacheEntry.Credential, true + } + return nil, false +} + +func (s *derivedKeyCache) retrieveFromCache(key string) (derivedKey, bool) { + if v, ok := s.values[key]; ok { + return v, true + } + return derivedKey{}, false +} + +// SigningKeyDeriver derives a signing key from a set of credentials +type SigningKeyDeriver struct { + cache derivedKeyCache +} + +// NewSigningKeyDeriver returns a new SigningKeyDeriver +func NewSigningKeyDeriver() *SigningKeyDeriver { + return &SigningKeyDeriver{ + cache: newDerivedKeyCache(), + } +} + +// DeriveKey returns a derived signing key from the given credentials to be used with SigV4 signing. +func (k *SigningKeyDeriver) DeriveKey(credential aws.Credentials, service, region string, signingTime SigningTime) []byte { + return k.cache.Get(credential, service, region, signingTime) +} + +func deriveKey(secret, service, region string, t SigningTime) []byte { + hmacDate := HMACSHA256([]byte("AWS4"+secret), []byte(t.ShortTimeFormat())) + hmacRegion := HMACSHA256(hmacDate, []byte(region)) + hmacService := HMACSHA256(hmacRegion, []byte(service)) + return HMACSHA256(hmacService, []byte("aws4_request")) +} + +func isSameDay(x, y time.Time) bool { + xYear, xMonth, xDay := x.Date() + yYear, yMonth, yDay := y.Date() + + if xYear != yYear { + return false + } + + if xMonth != yMonth { + return false + } + + return xDay == yDay +} diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/const.go b/api/auth/signer/v4sdk2/signer/internal/v4/const.go new file mode 100644 index 0000000..a23cb00 --- /dev/null +++ b/api/auth/signer/v4sdk2/signer/internal/v4/const.go @@ -0,0 +1,40 @@ +package v4 + +// Signature Version 4 (SigV4) Constants +const ( + // EmptyStringSHA256 is the hex encoded sha256 value of an empty string + EmptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` + + // UnsignedPayload indicates that the request payload body is unsigned + UnsignedPayload = "UNSIGNED-PAYLOAD" + + // AmzAlgorithmKey indicates the signing algorithm + AmzAlgorithmKey = "X-Amz-Algorithm" + + // AmzSecurityTokenKey indicates the security token to be used with temporary credentials + AmzSecurityTokenKey = "X-Amz-Security-Token" + + // AmzDateKey is the UTC timestamp for the request in the format YYYYMMDD'T'HHMMSS'Z' + AmzDateKey = "X-Amz-Date" + + // AmzCredentialKey is the access key ID and credential scope + AmzCredentialKey = "X-Amz-Credential" + + // AmzSignedHeadersKey is the set of headers signed for the request + AmzSignedHeadersKey = "X-Amz-SignedHeaders" + + // AmzSignatureKey is the query parameter to store the SigV4 signature + AmzSignatureKey = "X-Amz-Signature" + + // TimeFormat is the time format to be used in the X-Amz-Date header or query parameter + TimeFormat = "20060102T150405Z" + + // ShortTimeFormat is the shorten time format used in the credential scope + ShortTimeFormat = "20060102" + + // ContentSHAKey is the SHA256 of request body + ContentSHAKey = "X-Amz-Content-Sha256" + + // StreamingEventsPayload indicates that the request payload body is a signed event stream. + StreamingEventsPayload = "STREAMING-AWS4-HMAC-SHA256-EVENTS" +) diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/header_rules.go b/api/auth/signer/v4sdk2/signer/internal/v4/header_rules.go new file mode 100644 index 0000000..76fa34b --- /dev/null +++ b/api/auth/signer/v4sdk2/signer/internal/v4/header_rules.go @@ -0,0 +1,88 @@ +package v4 + +import ( + "strings" +) + +// Rules houses a set of Rule needed for validation of a +// string value +type Rules []Rule + +// Rule interface allows for more flexible rules and just simply +// checks whether or not a value adheres to that Rule +type Rule interface { + IsValid(value string) bool +} + +// IsValid will iterate through all rules and see if any rules +// apply to the value and supports nested rules +func (r Rules) IsValid(value string) bool { + for _, rule := range r { + if rule.IsValid(value) { + return true + } + } + return false +} + +// MapRule generic Rule for maps +type MapRule map[string]struct{} + +// IsValid for the map Rule satisfies whether it exists in the map +func (m MapRule) IsValid(value string) bool { + _, ok := m[value] + return ok +} + +// AllowList is a generic Rule for include listing +type AllowList struct { + Rule +} + +// IsValid for AllowList checks if the value is within the AllowList +func (w AllowList) IsValid(value string) bool { + return w.Rule.IsValid(value) +} + +// ExcludeList is a generic Rule for exclude listing +type ExcludeList struct { + Rule +} + +// IsValid for AllowList checks if the value is within the AllowList +func (b ExcludeList) IsValid(value string) bool { + return !b.Rule.IsValid(value) +} + +// Patterns is a list of strings to match against +type Patterns []string + +// IsValid for Patterns checks each pattern and returns if a match has +// been found +func (p Patterns) IsValid(value string) bool { + for _, pattern := range p { + if HasPrefixFold(value, pattern) { + return true + } + } + return false +} + +// InclusiveRules rules allow for rules to depend on one another +type InclusiveRules []Rule + +// IsValid will return true if all rules are true +func (r InclusiveRules) IsValid(value string) bool { + for _, rule := range r { + if !rule.IsValid(value) { + return false + } + } + return true +} + +// HasPrefixFold tests whether the string s begins with prefix, interpreted as UTF-8 strings, +// under Unicode case-folding. +func HasPrefixFold(s, prefix string) bool { + return len(s) >= len(prefix) && strings.EqualFold(s[0:len(prefix)], prefix) +} diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/headers.go b/api/auth/signer/v4sdk2/signer/internal/v4/headers.go new file mode 100644 index 0000000..f2a7766 --- /dev/null +++ b/api/auth/signer/v4sdk2/signer/internal/v4/headers.go @@ -0,0 +1,84 @@ +package v4 + +// IgnoredPresignedHeaders is a list of headers that are ignored during signing +var IgnoredPresignedHeaders = Rules{ + ExcludeList{ + MapRule{ + "Authorization": struct{}{}, + "User-Agent": struct{}{}, + "X-Amzn-Trace-Id": struct{}{}, + "Expect": struct{}{}, + }, + }, +} + +// IgnoredHeaders is a list of headers that are ignored during signing +// drop User-Agent header to be compatible with aws sdk java v1. +var IgnoredHeaders = Rules{ + ExcludeList{ + MapRule{ + "Authorization": struct{}{}, + //"User-Agent": struct{}{}, + "X-Amzn-Trace-Id": struct{}{}, + "Expect": struct{}{}, + }, + }, +} + +// RequiredSignedHeaders is a allow list for Build canonical headers. +var RequiredSignedHeaders = Rules{ + AllowList{ + MapRule{ + "Cache-Control": struct{}{}, + "Content-Disposition": struct{}{}, + "Content-Encoding": struct{}{}, + "Content-Language": struct{}{}, + "Content-Md5": struct{}{}, + "Content-Type": struct{}{}, + "Expires": struct{}{}, + "If-Match": struct{}{}, + "If-Modified-Since": struct{}{}, + "If-None-Match": struct{}{}, + "If-Unmodified-Since": struct{}{}, + "Range": struct{}{}, + "X-Amz-Acl": struct{}{}, + "X-Amz-Copy-Source": struct{}{}, + "X-Amz-Copy-Source-If-Match": struct{}{}, + "X-Amz-Copy-Source-If-Modified-Since": struct{}{}, + "X-Amz-Copy-Source-If-None-Match": struct{}{}, + "X-Amz-Copy-Source-If-Unmodified-Since": struct{}{}, + "X-Amz-Copy-Source-Range": struct{}{}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": struct{}{}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": struct{}{}, + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": struct{}{}, + "X-Amz-Expected-Bucket-Owner": struct{}{}, + "X-Amz-Grant-Full-control": struct{}{}, + "X-Amz-Grant-Read": struct{}{}, + "X-Amz-Grant-Read-Acp": struct{}{}, + "X-Amz-Grant-Write": struct{}{}, + "X-Amz-Grant-Write-Acp": struct{}{}, + "X-Amz-Metadata-Directive": struct{}{}, + "X-Amz-Mfa": struct{}{}, + "X-Amz-Request-Payer": struct{}{}, + "X-Amz-Server-Side-Encryption": struct{}{}, + "X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": struct{}{}, + "X-Amz-Server-Side-Encryption-Context": struct{}{}, + "X-Amz-Server-Side-Encryption-Customer-Algorithm": struct{}{}, + "X-Amz-Server-Side-Encryption-Customer-Key": struct{}{}, + "X-Amz-Server-Side-Encryption-Customer-Key-Md5": struct{}{}, + "X-Amz-Storage-Class": struct{}{}, + "X-Amz-Website-Redirect-Location": struct{}{}, + "X-Amz-Content-Sha256": struct{}{}, + "X-Amz-Tagging": struct{}{}, + }, + }, + Patterns{"X-Amz-Object-Lock-"}, + Patterns{"X-Amz-Meta-"}, +} + +// AllowedQueryHoisting is a allowed list for Build query headers. The boolean value +// represents whether or not it is a pattern. +var AllowedQueryHoisting = InclusiveRules{ + ExcludeList{RequiredSignedHeaders}, + Patterns{"X-Amz-"}, +} diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/headers_test.go b/api/auth/signer/v4sdk2/signer/internal/v4/headers_test.go new file mode 100644 index 0000000..6405ea9 --- /dev/null +++ b/api/auth/signer/v4sdk2/signer/internal/v4/headers_test.go @@ -0,0 +1,63 @@ +package v4 + +import "testing" + +func TestAllowedQueryHoisting(t *testing.T) { + cases := map[string]struct { + Header string + ExpectHoist bool + }{ + "object-lock": { + Header: "X-Amz-Object-Lock-Mode", + ExpectHoist: false, + }, + "s3 metadata": { + Header: "X-Amz-Meta-SomeName", + ExpectHoist: false, + }, + "another header": { + Header: "X-Amz-SomeOtherHeader", + ExpectHoist: true, + }, + "non X-AMZ header": { + Header: "X-SomeOtherHeader", + ExpectHoist: false, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + if e, a := c.ExpectHoist, AllowedQueryHoisting.IsValid(c.Header); e != a { + t.Errorf("expect hoist %v, was %v", e, a) + } + }) + } +} + +func TestIgnoredHeaders(t *testing.T) { + cases := map[string]struct { + Header string + ExpectIgnored bool + }{ + "expect": { + Header: "Expect", + ExpectIgnored: true, + }, + "authorization": { + Header: "Authorization", + ExpectIgnored: true, + }, + "X-AMZ header": { + Header: "X-Amz-Content-Sha256", + ExpectIgnored: false, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + if e, a := c.ExpectIgnored, IgnoredHeaders.IsValid(c.Header); e == a { + t.Errorf("expect ignored %v, was %v", e, a) + } + }) + } +} diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/hmac.go b/api/auth/signer/v4sdk2/signer/internal/v4/hmac.go new file mode 100644 index 0000000..e7fa7a1 --- /dev/null +++ b/api/auth/signer/v4sdk2/signer/internal/v4/hmac.go @@ -0,0 +1,13 @@ +package v4 + +import ( + "crypto/hmac" + "crypto/sha256" +) + +// HMACSHA256 computes a HMAC-SHA256 of data given the provided key. +func HMACSHA256(key []byte, data []byte) []byte { + hash := hmac.New(sha256.New, key) + hash.Write(data) + return hash.Sum(nil) +} diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/host.go b/api/auth/signer/v4sdk2/signer/internal/v4/host.go new file mode 100644 index 0000000..bf93659 --- /dev/null +++ b/api/auth/signer/v4sdk2/signer/internal/v4/host.go @@ -0,0 +1,75 @@ +package v4 + +import ( + "net/http" + "strings" +) + +// SanitizeHostForHeader removes default port from host and updates request.Host +func SanitizeHostForHeader(r *http.Request) { + host := getHost(r) + port := portOnly(host) + if port != "" && isDefaultPort(r.URL.Scheme, port) { + r.Host = stripPort(host) + } +} + +// Returns host from request +func getHost(r *http.Request) string { + if r.Host != "" { + return r.Host + } + + return r.URL.Host +} + +// Hostname returns u.Host, without any port number. +// +// If Host is an IPv6 literal with a port number, Hostname returns the +// IPv6 literal without the square brackets. IPv6 literals may include +// a zone identifier. +// +// Copied from the Go 1.8 standard library (net/url) +func stripPort(hostport string) string { + colon := strings.IndexByte(hostport, ':') + if colon == -1 { + return hostport + } + if i := strings.IndexByte(hostport, ']'); i != -1 { + return strings.TrimPrefix(hostport[:i], "[") + } + return hostport[:colon] +} + +// Port returns the port part of u.Host, without the leading colon. +// If u.Host doesn't contain a port, Port returns an empty string. +// +// Copied from the Go 1.8 standard library (net/url) +func portOnly(hostport string) string { + colon := strings.IndexByte(hostport, ':') + if colon == -1 { + return "" + } + if i := strings.Index(hostport, "]:"); i != -1 { + return hostport[i+len("]:"):] + } + if strings.Contains(hostport, "]") { + return "" + } + return hostport[colon+len(":"):] +} + +// Returns true if the specified URI is using the standard port +// (i.e. port 80 for HTTP URIs or 443 for HTTPS URIs) +func isDefaultPort(scheme, port string) bool { + if port == "" { + return true + } + + lowerCaseScheme := strings.ToLower(scheme) + if (lowerCaseScheme == "http" && port == "80") || (lowerCaseScheme == "https" && port == "443") { + return true + } + + return false +} diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/scope.go b/api/auth/signer/v4sdk2/signer/internal/v4/scope.go new file mode 100644 index 0000000..fc78879 --- /dev/null +++ b/api/auth/signer/v4sdk2/signer/internal/v4/scope.go @@ -0,0 +1,13 @@ +package v4 + +import "strings" + +// BuildCredentialScope builds the Signature Version 4 (SigV4) signing scope +func BuildCredentialScope(signingTime SigningTime, region, service string) string { + return strings.Join([]string{ + signingTime.ShortTimeFormat(), + region, + service, + "aws4_request", + }, "/") +} diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/time.go b/api/auth/signer/v4sdk2/signer/internal/v4/time.go new file mode 100644 index 0000000..1de06a7 --- /dev/null +++ b/api/auth/signer/v4sdk2/signer/internal/v4/time.go @@ -0,0 +1,36 @@ +package v4 + +import "time" + +// SigningTime provides a wrapper around a time.Time which provides cached values for SigV4 signing. +type SigningTime struct { + time.Time + timeFormat string + shortTimeFormat string +} + +// NewSigningTime creates a new SigningTime given a time.Time +func NewSigningTime(t time.Time) SigningTime { + return SigningTime{ + Time: t, + } +} + +// TimeFormat provides a time formatted in the X-Amz-Date format. +func (m *SigningTime) TimeFormat() string { + return m.format(&m.timeFormat, TimeFormat) +} + +// ShortTimeFormat provides a time formatted of 20060102. +func (m *SigningTime) ShortTimeFormat() string { + return m.format(&m.shortTimeFormat, ShortTimeFormat) +} + +func (m *SigningTime) format(target *string, format string) string { + if len(*target) > 0 { + return *target + } + v := m.Time.Format(format) + *target = v + return v +} diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/util.go b/api/auth/signer/v4sdk2/signer/internal/v4/util.go new file mode 100644 index 0000000..d025dba --- /dev/null +++ b/api/auth/signer/v4sdk2/signer/internal/v4/util.go @@ -0,0 +1,80 @@ +package v4 + +import ( + "net/url" + "strings" +) + +const doubleSpace = " " + +// StripExcessSpaces will rewrite the passed in slice's string values to not +// contain multiple side-by-side spaces. +func StripExcessSpaces(str string) string { + var j, k, l, m, spaces int + // Trim trailing spaces + for j = len(str) - 1; j >= 0 && str[j] == ' '; j-- { + } + + // Trim leading spaces + for k = 0; k < j && str[k] == ' '; k++ { + } + str = str[k : j+1] + + // Strip multiple spaces. + j = strings.Index(str, doubleSpace) + if j < 0 { + return str + } + + buf := []byte(str) + for k, m, l = j, j, len(buf); k < l; k++ { + if buf[k] == ' ' { + if spaces == 0 { + // First space. + buf[m] = buf[k] + m++ + } + spaces++ + } else { + // End of multiple spaces. + spaces = 0 + buf[m] = buf[k] + m++ + } + } + + return string(buf[:m]) +} + +// GetURIPath returns the escaped URI component from the provided URL. +func GetURIPath(u *url.URL) string { + var uriPath string + + if len(u.Opaque) > 0 { + const schemeSep, pathSep, queryStart = "//", "/", "?" + + opaque := u.Opaque + // Cut off the query string if present. + if idx := strings.Index(opaque, queryStart); idx >= 0 { + opaque = opaque[:idx] + } + + // Cutout the scheme separator if present. + if strings.Index(opaque, schemeSep) == 0 { + opaque = opaque[len(schemeSep):] + } + + // capture URI path starting with first path separator. + if idx := strings.Index(opaque, pathSep); idx >= 0 { + uriPath = opaque[idx:] + } + } else { + uriPath = u.EscapedPath() + } + + if len(uriPath) == 0 { + uriPath = "/" + } + + return uriPath +} diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/util_test.go b/api/auth/signer/v4sdk2/signer/internal/v4/util_test.go new file mode 100644 index 0000000..a38ef2d --- /dev/null +++ b/api/auth/signer/v4sdk2/signer/internal/v4/util_test.go @@ -0,0 +1,158 @@ +package v4 + +import ( + "net/http" + "net/url" + "testing" +) + +func lazyURLParse(v string) func() (*url.URL, error) { + return func() (*url.URL, error) { + return url.Parse(v) + } +} + +func TestGetURIPath(t *testing.T) { + cases := map[string]struct { + getURL func() (*url.URL, error) + expect string + }{ + // Cases + "with scheme": { + getURL: lazyURLParse("https://localhost:9000"), + expect: "/", + }, + "no port, with scheme": { + getURL: lazyURLParse("https://localhost"), + expect: "/", + }, + "without scheme": { + getURL: lazyURLParse("localhost:9000"), + expect: "/", + }, + "without scheme, with path": { + getURL: lazyURLParse("localhost:9000/abc123"), + expect: "/abc123", + }, + "without scheme, with separator": { + getURL: lazyURLParse("//localhost:9000"), + expect: "/", + }, + "no port, without scheme, with separator": { + getURL: lazyURLParse("//localhost"), + expect: "/", + }, + "without scheme, with separator, with path": { + getURL: lazyURLParse("//localhost:9000/abc123"), + expect: "/abc123", + }, + "no port, without scheme, with separator, with path": { + getURL: lazyURLParse("//localhost/abc123"), + expect: "/abc123", + }, + "opaque with query string": { + getURL: lazyURLParse("localhost:9000/abc123?efg=456"), + expect: "/abc123", + }, + "failing test": { + getURL: func() (*url.URL, error) { + endpoint := "https://service.region.amazonaws.com" + req, _ := http.NewRequest("POST", endpoint, nil) + u := req.URL + + u.Opaque = "//example.org/bucket/key-._~,!@#$%^&*()" + + query := u.Query() + query.Set("some-query-key", "value") + u.RawQuery = query.Encode() + + return u, nil + }, + expect: "/bucket/key-._~,!@#$%^&*()", + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + u, err := c.getURL() + if err != nil { + t.Fatalf("failed to get URL, %v", err) + } + + actual := GetURIPath(u) + if e, a := c.expect, actual; e != a { + t.Errorf("expect %v path, got %v", e, a) + } + }) + } +} + +func TestStripExcessHeaders(t *testing.T) { + vals := []string{ + "", + "123", + "1 2 3", + "1 2 3 ", + " 1 2 3", + "1 2 3", + "1 23", + "1 2 3", + "1 2 ", + " 1 2 ", + "12 3", + "12 3 1", + "12 3 1", + "12 3 1abc123", + } + + expected := []string{ + "", + "123", + "1 2 3", + "1 2 3", + "1 2 3", + "1 2 3", + "1 23", + "1 2 3", + "1 2", + "1 2", + "12 3", + "12 3 1", + "12 3 1", + "12 3 1abc123", + } + + for i := 0; i < len(vals); i++ { + r := StripExcessSpaces(vals[i]) + if e, a := expected[i], r; e != a { + t.Errorf("%d, expect %v, got %v", i, e, a) + } + } +} + +var stripExcessSpaceCases = []string{ + `AWS4-HMAC-SHA256 Credential=AKIDFAKEIDFAKEID/20160628/us-west-2/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=1234567890abcdef1234567890abcdef1234567890abcdef`, + `123 321 123 321`, + ` 123 321 123 321 `, + ` 123 321 123 321 `, + "123", + "1 2 3", + " 1 2 3", + "1 2 3", + "1 23", + "1 2 3", + "1 2 ", + " 1 2 ", + "12 3", + "12 3 1", + "12 3 1", + "12 3 1abc123", +} + +func BenchmarkStripExcessSpaces(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, v := range stripExcessSpaceCases { + StripExcessSpaces(v) + } + } +} diff --git a/api/auth/signer/v4sdk2/signer/v4/middleware.go b/api/auth/signer/v4sdk2/signer/v4/middleware.go new file mode 100644 index 0000000..82bf735 --- /dev/null +++ b/api/auth/signer/v4sdk2/signer/v4/middleware.go @@ -0,0 +1,442 @@ +package v4 + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "net/http" + "strings" + "time" + + v4Internal "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/internal/v4" + "github.com/aws/aws-sdk-go-v2/aws" + awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" + "github.com/aws/aws-sdk-go-v2/aws/middleware/private/metrics" + "github.com/aws/smithy-go/middleware" + smithyhttp "github.com/aws/smithy-go/transport/http" +) + +const computePayloadHashMiddlewareID = "ComputePayloadHash" + +// HashComputationError indicates an error occurred while computing the signing hash +type HashComputationError struct { + Err error +} + +// Error is the error message +func (e *HashComputationError) Error() string { + return fmt.Sprintf("failed to compute payload hash: %v", e.Err) +} + +// Unwrap returns the underlying error if one is set +func (e *HashComputationError) Unwrap() error { + return e.Err +} + +// SigningError indicates an error condition occurred while performing SigV4 signing +type SigningError struct { + Err error +} + +func (e *SigningError) Error() string { + return fmt.Sprintf("failed to sign request: %v", e.Err) +} + +// Unwrap returns the underlying error cause +func (e *SigningError) Unwrap() error { + return e.Err +} + +// UseDynamicPayloadSigningMiddleware swaps the compute payload sha256 middleware with a resolver middleware that +// switches between unsigned and signed payload based on TLS state for request. +// This middleware should not be used for AWS APIs that do not support unsigned payload signing auth. +// By default, SDK uses this middleware for known AWS APIs that support such TLS based auth selection . +// +// Usage example - +// S3 PutObject API allows unsigned payload signing auth usage when TLS is enabled, and uses this middleware to +// dynamically switch between unsigned and signed payload based on TLS state for request. +func UseDynamicPayloadSigningMiddleware(stack *middleware.Stack) error { + _, err := stack.Finalize.Swap(computePayloadHashMiddlewareID, &dynamicPayloadSigningMiddleware{}) + return err +} + +// dynamicPayloadSigningMiddleware dynamically resolves the middleware that computes and set payload sha256 middleware. +type dynamicPayloadSigningMiddleware struct { +} + +// ID returns the resolver identifier +func (m *dynamicPayloadSigningMiddleware) ID() string { + return computePayloadHashMiddlewareID +} + +// HandleFinalize delegates SHA256 computation according to whether the request +// is TLS-enabled. +func (m *dynamicPayloadSigningMiddleware) HandleFinalize( + ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler, +) ( + out middleware.FinalizeOutput, metadata middleware.Metadata, err error, +) { + req, ok := in.Request.(*smithyhttp.Request) + if !ok { + return out, metadata, fmt.Errorf("unknown transport type %T", in.Request) + } + + if req.IsHTTPS() { + return (&UnsignedPayload{}).HandleFinalize(ctx, in, next) + } + return (&ComputePayloadSHA256{}).HandleFinalize(ctx, in, next) +} + +// UnsignedPayload sets the SigV4 request payload hash to unsigned. +// +// Will not set the Unsigned Payload magic SHA value, if a SHA has already been +// stored in the context. (e.g. application pre-computed SHA256 before making +// API call). +// +// This middleware does not check the X-Amz-Content-Sha256 header, if that +// header is serialized a middleware must translate it into the context. +type UnsignedPayload struct{} + +// AddUnsignedPayloadMiddleware adds unsignedPayload to the operation +// middleware stack +func AddUnsignedPayloadMiddleware(stack *middleware.Stack) error { + return stack.Finalize.Insert(&UnsignedPayload{}, "ResolveEndpointV2", middleware.After) +} + +// ID returns the unsignedPayload identifier +func (m *UnsignedPayload) ID() string { + return computePayloadHashMiddlewareID +} + +// HandleFinalize sets the payload hash magic value to the unsigned sentinel. +func (m *UnsignedPayload) HandleFinalize( + ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler, +) ( + out middleware.FinalizeOutput, metadata middleware.Metadata, err error, +) { + if GetPayloadHash(ctx) == "" { + ctx = SetPayloadHash(ctx, v4Internal.UnsignedPayload) + } + return next.HandleFinalize(ctx, in) +} + +// ComputePayloadSHA256 computes SHA256 payload hash to sign. +// +// Will not set the Unsigned Payload magic SHA value, if a SHA has already been +// stored in the context. (e.g. application pre-computed SHA256 before making +// API call). +// +// This middleware does not check the X-Amz-Content-Sha256 header, if that +// header is serialized a middleware must translate it into the context. +type ComputePayloadSHA256 struct{} + +// AddComputePayloadSHA256Middleware adds computePayloadSHA256 to the +// operation middleware stack +func AddComputePayloadSHA256Middleware(stack *middleware.Stack) error { + return stack.Finalize.Insert(&ComputePayloadSHA256{}, "ResolveEndpointV2", middleware.After) +} + +// RemoveComputePayloadSHA256Middleware removes computePayloadSHA256 from the +// operation middleware stack +func RemoveComputePayloadSHA256Middleware(stack *middleware.Stack) error { + _, err := stack.Finalize.Remove(computePayloadHashMiddlewareID) + return err +} + +// ID is the middleware name +func (m *ComputePayloadSHA256) ID() string { + return computePayloadHashMiddlewareID +} + +// HandleFinalize computes the payload hash for the request, storing it to the +// context. This is a no-op if a caller has previously set that value. +func (m *ComputePayloadSHA256) HandleFinalize( + ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler, +) ( + out middleware.FinalizeOutput, metadata middleware.Metadata, err error, +) { + if GetPayloadHash(ctx) != "" { + return next.HandleFinalize(ctx, in) + } + + req, ok := in.Request.(*smithyhttp.Request) + if !ok { + return out, metadata, &HashComputationError{ + Err: fmt.Errorf("unexpected request middleware type %T", in.Request), + } + } + + hash := sha256.New() + if stream := req.GetStream(); stream != nil { + _, err = io.Copy(hash, stream) + if err != nil { + return out, metadata, &HashComputationError{ + Err: fmt.Errorf("failed to compute payload hash, %w", err), + } + } + + if err := req.RewindStream(); err != nil { + return out, metadata, &HashComputationError{ + Err: fmt.Errorf("failed to seek body to start, %w", err), + } + } + } + + ctx = SetPayloadHash(ctx, hex.EncodeToString(hash.Sum(nil))) + + return next.HandleFinalize(ctx, in) +} + +// SwapComputePayloadSHA256ForUnsignedPayloadMiddleware replaces the +// ComputePayloadSHA256 middleware with the UnsignedPayload middleware. +// +// Use this to disable computing the Payload SHA256 checksum and instead use +// UNSIGNED-PAYLOAD for the SHA256 value. +func SwapComputePayloadSHA256ForUnsignedPayloadMiddleware(stack *middleware.Stack) error { + _, err := stack.Finalize.Swap(computePayloadHashMiddlewareID, &UnsignedPayload{}) + return err +} + +// ContentSHA256Header sets the X-Amz-Content-Sha256 header value to +// the Payload hash stored in the context. +type ContentSHA256Header struct{} + +// AddContentSHA256HeaderMiddleware adds ContentSHA256Header to the +// operation middleware stack +func AddContentSHA256HeaderMiddleware(stack *middleware.Stack) error { + return stack.Finalize.Insert(&ContentSHA256Header{}, computePayloadHashMiddlewareID, middleware.After) +} + +// RemoveContentSHA256HeaderMiddleware removes contentSHA256Header middleware +// from the operation middleware stack +func RemoveContentSHA256HeaderMiddleware(stack *middleware.Stack) error { + _, err := stack.Finalize.Remove((*ContentSHA256Header)(nil).ID()) + return err +} + +// ID returns the ContentSHA256HeaderMiddleware identifier +func (m *ContentSHA256Header) ID() string { + return "SigV4ContentSHA256Header" +} + +// HandleFinalize sets the X-Amz-Content-Sha256 header value to the Payload hash +// stored in the context. +func (m *ContentSHA256Header) HandleFinalize( + ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler, +) ( + out middleware.FinalizeOutput, metadata middleware.Metadata, err error, +) { + req, ok := in.Request.(*smithyhttp.Request) + if !ok { + return out, metadata, &HashComputationError{Err: fmt.Errorf("unexpected request middleware type %T", in.Request)} + } + + req.Header.Set(v4Internal.ContentSHAKey, GetPayloadHash(ctx)) + return next.HandleFinalize(ctx, in) +} + +// SignHTTPRequestMiddlewareOptions is the configuration options for +// [SignHTTPRequestMiddleware]. +// +// Deprecated: [SignHTTPRequestMiddleware] is deprecated. +type SignHTTPRequestMiddlewareOptions struct { + CredentialsProvider aws.CredentialsProvider + Signer HTTPSigner + LogSigning bool +} + +// SignHTTPRequestMiddleware is a `FinalizeMiddleware` implementation for SigV4 +// HTTP Signing. +// +// Deprecated: AWS service clients no longer use this middleware. Signing as an +// SDK operation is now performed through an internal per-service middleware +// which opaquely selects and uses the signer from the resolved auth scheme. +type SignHTTPRequestMiddleware struct { + credentialsProvider aws.CredentialsProvider + signer HTTPSigner + logSigning bool +} + +// NewSignHTTPRequestMiddleware constructs a [SignHTTPRequestMiddleware] using +// the given [Signer] for signing requests. +// +// Deprecated: SignHTTPRequestMiddleware is deprecated. +func NewSignHTTPRequestMiddleware(options SignHTTPRequestMiddlewareOptions) *SignHTTPRequestMiddleware { + return &SignHTTPRequestMiddleware{ + credentialsProvider: options.CredentialsProvider, + signer: options.Signer, + logSigning: options.LogSigning, + } +} + +// ID is the SignHTTPRequestMiddleware identifier. +// +// Deprecated: SignHTTPRequestMiddleware is deprecated. +func (s *SignHTTPRequestMiddleware) ID() string { + return "Signing" +} + +// HandleFinalize will take the provided input and sign the request using the +// SigV4 authentication scheme. +// +// Deprecated: SignHTTPRequestMiddleware is deprecated. +func (s *SignHTTPRequestMiddleware) HandleFinalize(ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler) ( + out middleware.FinalizeOutput, metadata middleware.Metadata, err error, +) { + if !haveCredentialProvider(s.credentialsProvider) { + return next.HandleFinalize(ctx, in) + } + + req, ok := in.Request.(*smithyhttp.Request) + if !ok { + return out, metadata, &SigningError{Err: fmt.Errorf("unexpected request middleware type %T", in.Request)} + } + + signingName, signingRegion := awsmiddleware.GetSigningName(ctx), awsmiddleware.GetSigningRegion(ctx) + payloadHash := GetPayloadHash(ctx) + if len(payloadHash) == 0 { + return out, metadata, &SigningError{Err: fmt.Errorf("computed payload hash missing from context")} + } + + mctx := metrics.Context(ctx) + + if mctx != nil { + if attempt, err := mctx.Data().LatestAttempt(); err == nil { + attempt.CredentialFetchStartTime = time.Now() + } + } + + credentials, err := s.credentialsProvider.Retrieve(ctx) + + if mctx != nil { + if attempt, err := mctx.Data().LatestAttempt(); err == nil { + attempt.CredentialFetchEndTime = time.Now() + } + } + + if err != nil { + return out, metadata, &SigningError{Err: fmt.Errorf("failed to retrieve credentials: %w", err)} + } + + signerOptions := []func(o *SignerOptions){ + func(o *SignerOptions) { + o.Logger = middleware.GetLogger(ctx) + o.LogSigning = s.logSigning + }, + } + + // existing DisableURIPathEscaping is equivalent in purpose + // to authentication scheme property DisableDoubleEncoding + //disableDoubleEncoding, overridden := internalauth.GetDisableDoubleEncoding(ctx) // internalauth "github.com/aws/aws-sdk-go-v2/internal/auth" + //if overridden { + // signerOptions = append(signerOptions, func(o *SignerOptions) { + // o.DisableURIPathEscaping = disableDoubleEncoding + // }) + //} + + if mctx != nil { + if attempt, err := mctx.Data().LatestAttempt(); err == nil { + attempt.SignStartTime = time.Now() + } + } + + err = s.signer.SignHTTP(ctx, credentials, req.Request, payloadHash, signingName, signingRegion, time.Now(), signerOptions...) + + if mctx != nil { + if attempt, err := mctx.Data().LatestAttempt(); err == nil { + attempt.SignEndTime = time.Now() + } + } + + if err != nil { + return out, metadata, &SigningError{Err: fmt.Errorf("failed to sign http request, %w", err)} + } + + ctx = awsmiddleware.SetSigningCredentials(ctx, credentials) + + return next.HandleFinalize(ctx, in) +} + +// StreamingEventsPayload signs input event stream messages. +type StreamingEventsPayload struct{} + +// AddStreamingEventsPayload adds the streamingEventsPayload middleware to the stack. +func AddStreamingEventsPayload(stack *middleware.Stack) error { + return stack.Finalize.Add(&StreamingEventsPayload{}, middleware.Before) +} + +// ID identifies the middleware. +func (s *StreamingEventsPayload) ID() string { + return computePayloadHashMiddlewareID +} + +// HandleFinalize marks the input stream to be signed with SigV4. +func (s *StreamingEventsPayload) HandleFinalize( + ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler, +) ( + out middleware.FinalizeOutput, metadata middleware.Metadata, err error, +) { + contentSHA := GetPayloadHash(ctx) + if len(contentSHA) == 0 { + contentSHA = v4Internal.StreamingEventsPayload + } + + ctx = SetPayloadHash(ctx, contentSHA) + + return next.HandleFinalize(ctx, in) +} + +// GetSignedRequestSignature attempts to extract the signature of the request. +// Returning an error if the request is unsigned, or unable to extract the +// signature. +func GetSignedRequestSignature(r *http.Request) ([]byte, error) { + const authHeaderSignatureElem = "Signature=" + + if auth := r.Header.Get(authorizationHeader); len(auth) != 0 { + ps := strings.Split(auth, ", ") + for _, p := range ps { + if idx := strings.Index(p, authHeaderSignatureElem); idx >= 0 { + sig := p[len(authHeaderSignatureElem):] + if len(sig) == 0 { + return nil, fmt.Errorf("invalid request signature authorization header") + } + return hex.DecodeString(sig) + } + } + } + + if sig := r.URL.Query().Get("X-Amz-Signature"); len(sig) != 0 { + return hex.DecodeString(sig) + } + + return nil, fmt.Errorf("request not signed") +} + +func haveCredentialProvider(p aws.CredentialsProvider) bool { + if p == nil { + return false + } + + return !aws.IsCredentialsProvider(p, (*aws.AnonymousCredentials)(nil)) +} + +type payloadHashKey struct{} + +// GetPayloadHash retrieves the payload hash to use for signing +// +// Scoped to stack values. Use github.com/aws/smithy-go/middleware#ClearStackValues +// to clear all stack values. +func GetPayloadHash(ctx context.Context) (v string) { + v, _ = middleware.GetStackValue(ctx, payloadHashKey{}).(string) + return v +} + +// SetPayloadHash sets the payload hash to be used for signing the request +// +// Scoped to stack values. Use github.com/aws/smithy-go/middleware#ClearStackValues +// to clear all stack values. +func SetPayloadHash(ctx context.Context, hash string) context.Context { + return middleware.WithStackValue(ctx, payloadHashKey{}, hash) +} diff --git a/api/auth/signer/v4sdk2/signer/v4/presign_middleware.go b/api/auth/signer/v4sdk2/signer/v4/presign_middleware.go new file mode 100644 index 0000000..8522eb3 --- /dev/null +++ b/api/auth/signer/v4sdk2/signer/v4/presign_middleware.go @@ -0,0 +1,126 @@ +package v4 + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" + "github.com/aws/smithy-go/middleware" + smithyHTTP "github.com/aws/smithy-go/transport/http" +) + +// HTTPPresigner is an interface to a SigV4 signer that can sign create a +// presigned URL for a HTTP requests. +type HTTPPresigner interface { + PresignHTTP( + ctx context.Context, credentials aws.Credentials, r *http.Request, + payloadHash string, service string, region string, signingTime time.Time, + optFns ...func(*SignerOptions), + ) (url string, signedHeader http.Header, err error) +} + +// PresignedHTTPRequest provides the URL and signed headers that are included +// in the presigned URL. +type PresignedHTTPRequest struct { + URL string + Method string + SignedHeader http.Header +} + +// PresignHTTPRequestMiddlewareOptions is the options for the PresignHTTPRequestMiddleware middleware. +type PresignHTTPRequestMiddlewareOptions struct { + CredentialsProvider aws.CredentialsProvider + Presigner HTTPPresigner + LogSigning bool +} + +// PresignHTTPRequestMiddleware provides the Finalize middleware for creating a +// presigned URL for an HTTP request. +// +// Will short circuit the middleware stack and not forward onto the next +// Finalize handler. +type PresignHTTPRequestMiddleware struct { + credentialsProvider aws.CredentialsProvider + presigner HTTPPresigner + logSigning bool +} + +// NewPresignHTTPRequestMiddleware returns a new PresignHTTPRequestMiddleware +// initialized with the presigner. +func NewPresignHTTPRequestMiddleware(options PresignHTTPRequestMiddlewareOptions) *PresignHTTPRequestMiddleware { + return &PresignHTTPRequestMiddleware{ + credentialsProvider: options.CredentialsProvider, + presigner: options.Presigner, + logSigning: options.LogSigning, + } +} + +// ID provides the middleware ID. +func (*PresignHTTPRequestMiddleware) ID() string { return "PresignHTTPRequest" } + +// HandleFinalize will take the provided input and create a presigned url for +// the http request using the SigV4 presign authentication scheme. +// +// Since the signed request is not a valid HTTP request +func (s *PresignHTTPRequestMiddleware) HandleFinalize( + ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler, +) ( + out middleware.FinalizeOutput, metadata middleware.Metadata, err error, +) { + req, ok := in.Request.(*smithyHTTP.Request) + if !ok { + return out, metadata, &SigningError{ + Err: fmt.Errorf("unexpected request middleware type %T", in.Request), + } + } + + httpReq := req.Build(ctx) + if !haveCredentialProvider(s.credentialsProvider) { + out.Result = &PresignedHTTPRequest{ + URL: httpReq.URL.String(), + Method: httpReq.Method, + SignedHeader: http.Header{}, + } + + return out, metadata, nil + } + + signingName := awsmiddleware.GetSigningName(ctx) + signingRegion := awsmiddleware.GetSigningRegion(ctx) + payloadHash := GetPayloadHash(ctx) + if len(payloadHash) == 0 { + return out, metadata, &SigningError{ + Err: fmt.Errorf("computed payload hash missing from context"), + } + } + + credentials, err := s.credentialsProvider.Retrieve(ctx) + if err != nil { + return out, metadata, &SigningError{ + Err: fmt.Errorf("failed to retrieve credentials: %w", err), + } + } + + u, h, err := s.presigner.PresignHTTP(ctx, credentials, + httpReq, payloadHash, signingName, signingRegion, time.Now(), + func(o *SignerOptions) { + o.Logger = middleware.GetLogger(ctx) + o.LogSigning = s.logSigning + }) + if err != nil { + return out, metadata, &SigningError{ + Err: fmt.Errorf("failed to sign http request, %w", err), + } + } + + out.Result = &PresignedHTTPRequest{ + URL: u, + Method: httpReq.Method, + SignedHeader: h, + } + + return out, metadata, nil +} diff --git a/api/auth/signer/v4sdk2/signer/v4/stream.go b/api/auth/signer/v4sdk2/signer/v4/stream.go new file mode 100644 index 0000000..aa23387 --- /dev/null +++ b/api/auth/signer/v4sdk2/signer/v4/stream.go @@ -0,0 +1,87 @@ +package v4 + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "strings" + "time" + + v4Internal "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/internal/v4" + "github.com/aws/aws-sdk-go-v2/aws" +) + +// EventStreamSigner is an AWS EventStream protocol signer. +type EventStreamSigner interface { + GetSignature(ctx context.Context, headers, payload []byte, signingTime time.Time, optFns ...func(*StreamSignerOptions)) ([]byte, error) +} + +// StreamSignerOptions is the configuration options for StreamSigner. +type StreamSignerOptions struct{} + +// StreamSigner implements Signature Version 4 (SigV4) signing of event stream encoded payloads. +type StreamSigner struct { + options StreamSignerOptions + + credentials aws.Credentials + service string + region string + + prevSignature []byte + + signingKeyDeriver *v4Internal.SigningKeyDeriver +} + +// NewStreamSigner returns a new AWS EventStream protocol signer. +func NewStreamSigner(credentials aws.Credentials, service, region string, seedSignature []byte, optFns ...func(*StreamSignerOptions)) *StreamSigner { + o := StreamSignerOptions{} + + for _, fn := range optFns { + fn(&o) + } + + return &StreamSigner{ + options: o, + credentials: credentials, + service: service, + region: region, + signingKeyDeriver: v4Internal.NewSigningKeyDeriver(), + prevSignature: seedSignature, + } +} + +// GetSignature signs the provided header and payload bytes. +func (s *StreamSigner) GetSignature(ctx context.Context, headers, payload []byte, signingTime time.Time, optFns ...func(*StreamSignerOptions)) ([]byte, error) { + options := s.options + + for _, fn := range optFns { + fn(&options) + } + + prevSignature := s.prevSignature + + st := v4Internal.NewSigningTime(signingTime) + + sigKey := s.signingKeyDeriver.DeriveKey(s.credentials, s.service, s.region, st) + + scope := v4Internal.BuildCredentialScope(st, s.region, s.service) + + stringToSign := s.buildEventStreamStringToSign(headers, payload, prevSignature, scope, &st) + + signature := v4Internal.HMACSHA256(sigKey, []byte(stringToSign)) + s.prevSignature = signature + + return signature, nil +} + +func (s *StreamSigner) buildEventStreamStringToSign(headers, payload, previousSignature []byte, credentialScope string, signingTime *v4Internal.SigningTime) string { + hash := sha256.New() + return strings.Join([]string{ + "AWS4-HMAC-SHA256-PAYLOAD", + signingTime.TimeFormat(), + credentialScope, + hex.EncodeToString(previousSignature), + hex.EncodeToString(makeHash(hash, headers)), + hex.EncodeToString(makeHash(hash, payload)), + }, "\n") +} diff --git a/api/auth/signer/v4sdk2/signer/v4/v4.go b/api/auth/signer/v4sdk2/signer/v4/v4.go new file mode 100644 index 0000000..f79ed5c --- /dev/null +++ b/api/auth/signer/v4sdk2/signer/v4/v4.go @@ -0,0 +1,570 @@ +// Package v4 implements signing for AWS V4 signer +// +// Provides request signing for request that need to be signed with +// AWS V4 Signatures. +// +// # Standalone Signer +// +// Generally using the signer outside of the SDK should not require any additional +// +// The signer does this by taking advantage of the URL.EscapedPath method. If your request URI requires +// +// additional escaping you many need to use the URL.Opaque to define what the raw URI should be sent +// to the service as. +// +// The signer will first check the URL.Opaque field, and use its value if set. +// The signer does require the URL.Opaque field to be set in the form of: +// +// "///" +// +// // e.g. +// "//example.com/some/path" +// +// The leading "//" and hostname are required or the URL.Opaque escaping will +// not work correctly. +// +// If URL.Opaque is not set the signer will fallback to the URL.EscapedPath() +// method and using the returned value. +// +// AWS v4 signature validation requires that the canonical string's URI path +// element must be the URI escaped form of the HTTP request's path. +// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html +// +// The Go HTTP client will perform escaping automatically on the request. Some +// of these escaping may cause signature validation errors because the HTTP +// request differs from the URI path or query that the signature was generated. +// https://golang.org/pkg/net/url/#URL.EscapedPath +// +// Because of this, it is recommended that when using the signer outside of the +// SDK that explicitly escaping the request prior to being signed is preferable, +// and will help prevent signature validation errors. This can be done by setting +// the URL.Opaque or URL.RawPath. The SDK will use URL.Opaque first and then +// call URL.EscapedPath() if Opaque is not set. +// +// Test `TestStandaloneSign` provides a complete example of using the signer +// outside of the SDK and pre-escaping the URI path. +package v4 + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "hash" + "net/http" + "net/textproto" + "net/url" + "sort" + "strconv" + "strings" + "time" + + v4Internal "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/internal/v4" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/smithy-go/encoding/httpbinding" + "github.com/aws/smithy-go/logging" +) + +const ( + signingAlgorithm = "AWS4-HMAC-SHA256" + authorizationHeader = "Authorization" + + // Version of signing v4 + Version = "SigV4" +) + +// HTTPSigner is an interface to a SigV4 signer that can sign HTTP requests +type HTTPSigner interface { + SignHTTP(ctx context.Context, credentials aws.Credentials, r *http.Request, payloadHash string, service string, region string, signingTime time.Time, optFns ...func(*SignerOptions)) error +} + +type keyDerivator interface { + DeriveKey(credential aws.Credentials, service, region string, signingTime v4Internal.SigningTime) []byte +} + +// SignerOptions is the SigV4 Signer options. +type SignerOptions struct { + // Disables the Signer's moving HTTP header key/value pairs from the HTTP + // request header to the request's query string. This is most commonly used + // with pre-signed requests preventing headers from being added to the + // request's query string. + DisableHeaderHoisting bool + + // Disables the automatic escaping of the URI path of the request for the + // siganture's canonical string's path. For services that do not need additional + // escaping then use this to disable the signer escaping the path. + // + // S3 is an example of a service that does not need additional escaping. + // + // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html + DisableURIPathEscaping bool + + // The logger to send log messages to. + Logger logging.Logger + + // Enable logging of signed requests. + // This will enable logging of the canonical request, the string to sign, and for presigning the subsequent + // presigned URL. + LogSigning bool + + // Disables setting the session token on the request as part of signing + // through X-Amz-Security-Token. This is needed for variations of v4 that + // present the token elsewhere. + DisableSessionToken bool +} + +// Signer applies AWS v4 signing to given request. Use this to sign requests +// that need to be signed with AWS V4 Signatures. +type Signer struct { + options SignerOptions + keyDerivator keyDerivator +} + +// NewSigner returns a new SigV4 Signer +func NewSigner(optFns ...func(signer *SignerOptions)) *Signer { + options := SignerOptions{} + + for _, fn := range optFns { + fn(&options) + } + + return &Signer{options: options, keyDerivator: v4Internal.NewSigningKeyDeriver()} +} + +type httpSigner struct { + Request *http.Request + ServiceName string + Region string + Time v4Internal.SigningTime + Credentials aws.Credentials + KeyDerivator keyDerivator + IsPreSign bool + + PayloadHash string + + DisableHeaderHoisting bool + DisableURIPathEscaping bool + DisableSessionToken bool +} + +func (s *httpSigner) Build() (signedRequest, error) { + req := s.Request + + query := req.URL.Query() + headers := req.Header + + s.setRequiredSigningFields(headers, query) + + // Sort Each Query Key's Values + for key := range query { + sort.Strings(query[key]) + } + + v4Internal.SanitizeHostForHeader(req) + + credentialScope := s.buildCredentialScope() + credentialStr := s.Credentials.AccessKeyID + "/" + credentialScope + if s.IsPreSign { + query.Set(v4Internal.AmzCredentialKey, credentialStr) + } + + unsignedHeaders := headers + if s.IsPreSign && !s.DisableHeaderHoisting { + var urlValues url.Values + urlValues, unsignedHeaders = buildQuery(v4Internal.AllowedQueryHoisting, headers) + for k := range urlValues { + query[k] = urlValues[k] + } + } + + host := req.URL.Host + if len(req.Host) > 0 { + host = req.Host + } + + var ( + signedHeaders http.Header + signedHeadersStr string + canonicalHeaderStr string + ) + + if s.IsPreSign { + signedHeaders, signedHeadersStr, canonicalHeaderStr = s.buildCanonicalHeaders(host, v4Internal.IgnoredPresignedHeaders, unsignedHeaders, s.Request.ContentLength) + } else { + signedHeaders, signedHeadersStr, canonicalHeaderStr = s.buildCanonicalHeaders(host, v4Internal.IgnoredHeaders, unsignedHeaders, s.Request.ContentLength) + } + + if s.IsPreSign { + query.Set(v4Internal.AmzSignedHeadersKey, signedHeadersStr) + } + + var rawQuery strings.Builder + rawQuery.WriteString(strings.Replace(query.Encode(), "+", "%20", -1)) + + canonicalURI := v4Internal.GetURIPath(req.URL) + if !s.DisableURIPathEscaping { + canonicalURI = httpbinding.EscapePath(canonicalURI, false) + } + + canonicalString := s.buildCanonicalString( + req.Method, + canonicalURI, + rawQuery.String(), + signedHeadersStr, + canonicalHeaderStr, + ) + + strToSign := s.buildStringToSign(credentialScope, canonicalString) + signingSignature, err := s.buildSignature(strToSign) + if err != nil { + return signedRequest{}, err + } + + if s.IsPreSign { + rawQuery.WriteString("&X-Amz-Signature=") + rawQuery.WriteString(signingSignature) + } else { + headers[authorizationHeader] = append(headers[authorizationHeader][:0], buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature)) + } + + req.URL.RawQuery = rawQuery.String() + + return signedRequest{ + Request: req, + SignedHeaders: signedHeaders, + CanonicalString: canonicalString, + StringToSign: strToSign, + PreSigned: s.IsPreSign, + }, nil +} + +func buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature string) string { + const credential = "Credential=" + const signedHeaders = "SignedHeaders=" + const signature = "Signature=" + const commaSpace = ", " + + var parts strings.Builder + parts.Grow(len(signingAlgorithm) + 1 + + len(credential) + len(credentialStr) + 2 + + len(signedHeaders) + len(signedHeadersStr) + 2 + + len(signature) + len(signingSignature), + ) + parts.WriteString(signingAlgorithm) + parts.WriteRune(' ') + parts.WriteString(credential) + parts.WriteString(credentialStr) + parts.WriteString(commaSpace) + parts.WriteString(signedHeaders) + parts.WriteString(signedHeadersStr) + parts.WriteString(commaSpace) + parts.WriteString(signature) + parts.WriteString(signingSignature) + return parts.String() +} + +// SignHTTP signs AWS v4 requests with the provided payload hash, service name, region the +// request is made to, and time the request is signed at. The signTime allows +// you to specify that a request is signed for the future, and cannot be +// used until then. +// +// The payloadHash is the hex encoded SHA-256 hash of the request payload, and +// must be provided. Even if the request has no payload (aka body). If the +// request has no payload you should use the hex encoded SHA-256 of an empty +// string as the payloadHash value. +// +// "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +// +// Some services such as Amazon S3 accept alternative values for the payload +// hash, such as "UNSIGNED-PAYLOAD" for requests where the body will not be +// included in the request signature. +// +// https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html +// +// Sign differs from Presign in that it will sign the request using HTTP +// header values. This type of signing is intended for http.Request values that +// will not be shared, or are shared in a way the header values on the request +// will not be lost. +// +// The passed in request will be modified in place. +func (s Signer) SignHTTP(ctx context.Context, credentials aws.Credentials, r *http.Request, payloadHash string, service string, region string, signingTime time.Time, optFns ...func(options *SignerOptions)) error { + options := s.options + + for _, fn := range optFns { + fn(&options) + } + + signer := &httpSigner{ + Request: r, + PayloadHash: payloadHash, + ServiceName: service, + Region: region, + Credentials: credentials, + Time: v4Internal.NewSigningTime(signingTime.UTC()), + DisableHeaderHoisting: options.DisableHeaderHoisting, + DisableURIPathEscaping: options.DisableURIPathEscaping, + DisableSessionToken: options.DisableSessionToken, + KeyDerivator: s.keyDerivator, + } + + signedRequest, err := signer.Build() + if err != nil { + return err + } + + logSigningInfo(ctx, options, &signedRequest, false) + + return nil +} + +// PresignHTTP signs AWS v4 requests with the payload hash, service name, region +// the request is made to, and time the request is signed at. The signTime +// allows you to specify that a request is signed for the future, and cannot +// be used until then. +// +// Returns the signed URL and the map of HTTP headers that were included in the +// signature or an error if signing the request failed. For presigned requests +// these headers and their values must be included on the HTTP request when it +// is made. This is helpful to know what header values need to be shared with +// the party the presigned request will be distributed to. +// +// The payloadHash is the hex encoded SHA-256 hash of the request payload, and +// must be provided. Even if the request has no payload (aka body). If the +// request has no payload you should use the hex encoded SHA-256 of an empty +// string as the payloadHash value. +// +// "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +// +// Some services such as Amazon S3 accept alternative values for the payload +// hash, such as "UNSIGNED-PAYLOAD" for requests where the body will not be +// included in the request signature. +// +// https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html +// +// PresignHTTP differs from SignHTTP in that it will sign the request using +// query string instead of header values. This allows you to share the +// Presigned Request's URL with third parties, or distribute it throughout your +// system with minimal dependencies. +// +// PresignHTTP will not set the expires time of the presigned request +// automatically. To specify the expire duration for a request add the +// "X-Amz-Expires" query parameter on the request with the value as the +// duration in seconds the presigned URL should be considered valid for. This +// parameter is not used by all AWS services, and is most notable used by +// Amazon S3 APIs. +// +// expires := 20 * time.Minute +// query := req.URL.Query() +// query.Set("X-Amz-Expires", strconv.FormatInt(int64(expires/time.Second), 10)) +// req.URL.RawQuery = query.Encode() +// +// This method does not modify the provided request. +func (s *Signer) PresignHTTP( + ctx context.Context, credentials aws.Credentials, r *http.Request, + payloadHash string, service string, region string, signingTime time.Time, + optFns ...func(*SignerOptions), +) (signedURI string, signedHeaders http.Header, err error) { + options := s.options + + for _, fn := range optFns { + fn(&options) + } + + signer := &httpSigner{ + Request: r.Clone(r.Context()), + PayloadHash: payloadHash, + ServiceName: service, + Region: region, + Credentials: credentials, + Time: v4Internal.NewSigningTime(signingTime.UTC()), + IsPreSign: true, + DisableHeaderHoisting: options.DisableHeaderHoisting, + DisableURIPathEscaping: options.DisableURIPathEscaping, + DisableSessionToken: options.DisableSessionToken, + KeyDerivator: s.keyDerivator, + } + + signedRequest, err := signer.Build() + if err != nil { + return "", nil, err + } + + logSigningInfo(ctx, options, &signedRequest, true) + + signedHeaders = make(http.Header) + + // For the signed headers we canonicalize the header keys in the returned map. + // This avoids situations where can standard library double headers like host header. For example the standard + // library will set the Host header, even if it is present in lower-case form. + for k, v := range signedRequest.SignedHeaders { + key := textproto.CanonicalMIMEHeaderKey(k) + signedHeaders[key] = append(signedHeaders[key], v...) + } + + return signedRequest.Request.URL.String(), signedHeaders, nil +} + +func (s *httpSigner) buildCredentialScope() string { + return v4Internal.BuildCredentialScope(s.Time, s.Region, s.ServiceName) +} + +func buildQuery(r v4Internal.Rule, header http.Header) (url.Values, http.Header) { + query := url.Values{} + unsignedHeaders := http.Header{} + for k, h := range header { + if r.IsValid(k) { + query[k] = h + } else { + unsignedHeaders[k] = h + } + } + + return query, unsignedHeaders +} + +func (s *httpSigner) buildCanonicalHeaders(host string, rule v4Internal.Rule, header http.Header, length int64) (signed http.Header, signedHeaders, canonicalHeadersStr string) { + signed = make(http.Header) + + var headers []string + const hostHeader = "host" + headers = append(headers, hostHeader) + signed[hostHeader] = append(signed[hostHeader], host) + + const contentLengthHeader = "content-length" + if length > 0 { + headers = append(headers, contentLengthHeader) + signed[contentLengthHeader] = append(signed[contentLengthHeader], strconv.FormatInt(length, 10)) + } + + for k, v := range header { + if !rule.IsValid(k) { + continue // ignored header + } + if strings.EqualFold(k, contentLengthHeader) { + // prevent signing already handled content-length header. + continue + } + + lowerCaseKey := strings.ToLower(k) + if _, ok := signed[lowerCaseKey]; ok { + // include additional values + signed[lowerCaseKey] = append(signed[lowerCaseKey], v...) + continue + } + + headers = append(headers, lowerCaseKey) + signed[lowerCaseKey] = v + } + sort.Strings(headers) + + signedHeaders = strings.Join(headers, ";") + + var canonicalHeaders strings.Builder + n := len(headers) + const colon = ':' + for i := 0; i < n; i++ { + if headers[i] == hostHeader { + canonicalHeaders.WriteString(hostHeader) + canonicalHeaders.WriteRune(colon) + canonicalHeaders.WriteString(v4Internal.StripExcessSpaces(host)) + } else { + canonicalHeaders.WriteString(headers[i]) + canonicalHeaders.WriteRune(colon) + // Trim out leading, trailing, and dedup inner spaces from signed header values. + values := signed[headers[i]] + for j, v := range values { + cleanedValue := strings.TrimSpace(v4Internal.StripExcessSpaces(v)) + canonicalHeaders.WriteString(cleanedValue) + if j < len(values)-1 { + canonicalHeaders.WriteRune(',') + } + } + } + canonicalHeaders.WriteRune('\n') + } + canonicalHeadersStr = canonicalHeaders.String() + + return signed, signedHeaders, canonicalHeadersStr +} + +func (s *httpSigner) buildCanonicalString(method, uri, query, signedHeaders, canonicalHeaders string) string { + return strings.Join([]string{ + method, + uri, + query, + canonicalHeaders, + signedHeaders, + s.PayloadHash, + }, "\n") +} + +func (s *httpSigner) buildStringToSign(credentialScope, canonicalRequestString string) string { + return strings.Join([]string{ + signingAlgorithm, + s.Time.TimeFormat(), + credentialScope, + hex.EncodeToString(makeHash(sha256.New(), []byte(canonicalRequestString))), + }, "\n") +} + +func makeHash(hash hash.Hash, b []byte) []byte { + hash.Reset() + hash.Write(b) + return hash.Sum(nil) +} + +func (s *httpSigner) buildSignature(strToSign string) (string, error) { + key := s.KeyDerivator.DeriveKey(s.Credentials, s.ServiceName, s.Region, s.Time) + return hex.EncodeToString(v4Internal.HMACSHA256(key, []byte(strToSign))), nil +} + +func (s *httpSigner) setRequiredSigningFields(headers http.Header, query url.Values) { + amzDate := s.Time.TimeFormat() + + if s.IsPreSign { + query.Set(v4Internal.AmzAlgorithmKey, signingAlgorithm) + sessionToken := s.Credentials.SessionToken + if !s.DisableSessionToken && len(sessionToken) > 0 { + query.Set("X-Amz-Security-Token", sessionToken) + } + + query.Set(v4Internal.AmzDateKey, amzDate) + return + } + + headers[v4Internal.AmzDateKey] = append(headers[v4Internal.AmzDateKey][:0], amzDate) + + if !s.DisableSessionToken && len(s.Credentials.SessionToken) > 0 { + headers[v4Internal.AmzSecurityTokenKey] = append(headers[v4Internal.AmzSecurityTokenKey][:0], s.Credentials.SessionToken) + } +} + +func logSigningInfo(ctx context.Context, options SignerOptions, request *signedRequest, isPresign bool) { + if !options.LogSigning { + return + } + signedURLMsg := "" + if isPresign { + signedURLMsg = fmt.Sprintf(logSignedURLMsg, request.Request.URL.String()) + } + logger := logging.WithContext(ctx, options.Logger) + logger.Logf(logging.Debug, logSignInfoMsg, request.CanonicalString, request.StringToSign, signedURLMsg) +} + +type signedRequest struct { + Request *http.Request + SignedHeaders http.Header + CanonicalString string + StringToSign string + PreSigned bool +} + +const logSignInfoMsg = `Request Signature: +---[ CANONICAL STRING ]----------------------------- +%s +---[ STRING TO SIGN ]-------------------------------- +%s%s +-----------------------------------------------------` +const logSignedURLMsg = ` +---[ SIGNED URL ]------------------------------------ +%s` diff --git a/api/auth/signer/v4sdk2/signer/v4/v4_test.go b/api/auth/signer/v4sdk2/signer/v4/v4_test.go new file mode 100644 index 0000000..420afd9 --- /dev/null +++ b/api/auth/signer/v4sdk2/signer/v4/v4_test.go @@ -0,0 +1,363 @@ +package v4 + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "reflect" + "strings" + "testing" + "time" + + v4Internal "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/internal/v4" + "github.com/aws/aws-sdk-go-v2/aws" +) + +var testCredentials = aws.Credentials{AccessKeyID: "AKID", SecretAccessKey: "SECRET", SessionToken: "SESSION"} + +func buildRequest(serviceName, region, body string) (*http.Request, string) { + reader := strings.NewReader(body) + return buildRequestWithBodyReader(serviceName, region, reader) +} + +func buildRequestWithBodyReader(serviceName, region string, body io.Reader) (*http.Request, string) { + var bodyLen int + + type lenner interface { + Len() int + } + if lr, ok := body.(lenner); ok { + bodyLen = lr.Len() + } + + endpoint := "https://" + serviceName + "." + region + ".amazonaws.com" + req, _ := http.NewRequest("POST", endpoint, body) + req.URL.Opaque = "//example.org/bucket/key-._~,!@#$%^&*()" + req.Header.Set("X-Amz-Target", "prefix.Operation") + req.Header.Set("Content-Type", "application/x-amz-json-1.0") + + if bodyLen > 0 { + req.ContentLength = int64(bodyLen) + } + + req.Header.Set("X-Amz-Meta-Other-Header", "some-value=!@#$%^&* (+)") + req.Header.Add("X-Amz-Meta-Other-Header_With_Underscore", "some-value=!@#$%^&* (+)") + req.Header.Add("X-amz-Meta-Other-Header_With_Underscore", "some-value=!@#$%^&* (+)") + + h := sha256.New() + _, _ = io.Copy(h, body) + payloadHash := hex.EncodeToString(h.Sum(nil)) + + return req, payloadHash +} + +func TestPresignRequest(t *testing.T) { + req, body := buildRequest("dynamodb", "us-east-1", "{}") + + query := req.URL.Query() + query.Set("X-Amz-Expires", "300") + req.URL.RawQuery = query.Encode() + + signer := NewSigner() + signed, headers, err := signer.PresignHTTP(context.Background(), testCredentials, req, body, "dynamodb", "us-east-1", time.Unix(0, 0)) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expectedDate := "19700101T000000Z" + expectedHeaders := "content-length;content-type;host;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore" + expectedSig := "122f0b9e091e4ba84286097e2b3404a1f1f4c4aad479adda95b7dff0ccbe5581" + expectedCred := "AKID/19700101/us-east-1/dynamodb/aws4_request" + expectedTarget := "prefix.Operation" + + q, err := url.ParseQuery(signed[strings.Index(signed, "?"):]) + if err != nil { + t.Errorf("expect no error, got %v", err) + } + + if e, a := expectedSig, q.Get("X-Amz-Signature"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := expectedCred, q.Get("X-Amz-Credential"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := expectedHeaders, q.Get("X-Amz-SignedHeaders"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := expectedDate, q.Get("X-Amz-Date"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if a := q.Get("X-Amz-Meta-Other-Header"); len(a) != 0 { + t.Errorf("expect %v to be empty", a) + } + if e, a := expectedTarget, q.Get("X-Amz-Target"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + + for _, h := range strings.Split(expectedHeaders, ";") { + v := headers.Get(h) + if len(v) == 0 { + t.Errorf("expect %v, to be present in header map", h) + } + } +} + +func TestPresignBodyWithArrayRequest(t *testing.T) { + req, body := buildRequest("dynamodb", "us-east-1", "{}") + req.URL.RawQuery = "Foo=z&Foo=o&Foo=m&Foo=a" + + query := req.URL.Query() + query.Set("X-Amz-Expires", "300") + req.URL.RawQuery = query.Encode() + + signer := NewSigner() + signed, headers, err := signer.PresignHTTP(context.Background(), testCredentials, req, body, "dynamodb", "us-east-1", time.Unix(0, 0)) + if err != nil { + t.Fatalf("expect no error, got %v", err) + } + + q, err := url.ParseQuery(signed[strings.Index(signed, "?"):]) + if err != nil { + t.Errorf("expect no error, got %v", err) + } + + expectedDate := "19700101T000000Z" + expectedHeaders := "content-length;content-type;host;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore" + expectedSig := "e3ac55addee8711b76c6d608d762cff285fe8b627a057f8b5ec9268cf82c08b1" + expectedCred := "AKID/19700101/us-east-1/dynamodb/aws4_request" + expectedTarget := "prefix.Operation" + + if e, a := expectedSig, q.Get("X-Amz-Signature"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := expectedCred, q.Get("X-Amz-Credential"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := expectedHeaders, q.Get("X-Amz-SignedHeaders"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := expectedDate, q.Get("X-Amz-Date"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if a := q.Get("X-Amz-Meta-Other-Header"); len(a) != 0 { + t.Errorf("expect %v to be empty, was not", a) + } + if e, a := expectedTarget, q.Get("X-Amz-Target"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + + for _, h := range strings.Split(expectedHeaders, ";") { + v := headers.Get(h) + if len(v) == 0 { + t.Errorf("expect %v, to be present in header map", h) + } + } +} + +func TestSignRequest(t *testing.T) { + req, body := buildRequest("dynamodb", "us-east-1", "{}") + signer := NewSigner() + err := signer.SignHTTP(context.Background(), testCredentials, req, body, "dynamodb", "us-east-1", time.Unix(0, 0)) + if err != nil { + t.Fatalf("expect no error, got %v", err) + } + + expectedDate := "19700101T000000Z" + expectedSig := "AWS4-HMAC-SHA256 Credential=AKID/19700101/us-east-1/dynamodb/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore;x-amz-security-token;x-amz-target, Signature=a518299330494908a70222cec6899f6f32f297f8595f6df1776d998936652ad9" + + q := req.Header + if e, a := expectedSig, q.Get("Authorization"); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := expectedDate, q.Get("X-Amz-Date"); e != a { + t.Errorf("expect %v, got %v", e, a) + } +} + +func TestBuildCanonicalRequest(t *testing.T) { + req, _ := buildRequest("dynamodb", "us-east-1", "{}") + req.URL.RawQuery = "Foo=z&Foo=o&Foo=m&Foo=a" + + ctx := &httpSigner{ + ServiceName: "dynamodb", + Region: "us-east-1", + Request: req, + Time: v4Internal.NewSigningTime(time.Now()), + KeyDerivator: v4Internal.NewSigningKeyDeriver(), + } + + build, err := ctx.Build() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected := "https://example.org/bucket/key-._~,!@#$%^&*()?Foo=a&Foo=m&Foo=o&Foo=z" + if e, a := expected, build.Request.URL.String(); e != a { + t.Errorf("expect %v, got %v", e, a) + } +} + +func TestSigner_SignHTTP_NoReplaceRequestBody(t *testing.T) { + req, bodyHash := buildRequest("dynamodb", "us-east-1", "{}") + req.Body = ioutil.NopCloser(bytes.NewReader([]byte{})) + + s := NewSigner() + + origBody := req.Body + + err := s.SignHTTP(context.Background(), testCredentials, req, bodyHash, "dynamodb", "us-east-1", time.Now()) + if err != nil { + t.Fatalf("expect no error, got %v", err) + } + + if req.Body != origBody { + t.Errorf("expect request body to not be chagned") + } +} + +func TestRequestHost(t *testing.T) { + req, _ := buildRequest("dynamodb", "us-east-1", "{}") + req.URL.RawQuery = "Foo=z&Foo=o&Foo=m&Foo=a" + req.Host = "myhost" + + query := req.URL.Query() + query.Set("X-Amz-Expires", "5") + req.URL.RawQuery = query.Encode() + + ctx := &httpSigner{ + ServiceName: "dynamodb", + Region: "us-east-1", + Request: req, + Time: v4Internal.NewSigningTime(time.Now()), + KeyDerivator: v4Internal.NewSigningKeyDeriver(), + } + + build, err := ctx.Build() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if !strings.Contains(build.CanonicalString, "host:"+req.Host) { + t.Errorf("canonical host header invalid") + } +} + +func TestSign_buildCanonicalHeadersContentLengthPresent(t *testing.T) { + body := `{"description": "this is a test"}` + req, _ := buildRequest("dynamodb", "us-east-1", body) + req.URL.RawQuery = "Foo=z&Foo=o&Foo=m&Foo=a" + req.Host = "myhost" + + contentLength := fmt.Sprintf("%d", len([]byte(body))) + req.Header.Add("Content-Length", contentLength) + + query := req.URL.Query() + query.Set("X-Amz-Expires", "5") + req.URL.RawQuery = query.Encode() + + ctx := &httpSigner{ + ServiceName: "dynamodb", + Region: "us-east-1", + Request: req, + Time: v4Internal.NewSigningTime(time.Now()), + KeyDerivator: v4Internal.NewSigningKeyDeriver(), + } + + build, err := ctx.Build() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if !strings.Contains(build.CanonicalString, "content-length:"+contentLength+"\n") { + t.Errorf("canonical header content-length invalid") + } +} + +func TestSign_buildCanonicalHeaders(t *testing.T) { + serviceName := "mockAPI" + region := "mock-region" + endpoint := "https://" + serviceName + "." + region + ".amazonaws.com" + + req, err := http.NewRequest("POST", endpoint, nil) + if err != nil { + t.Fatalf("failed to create request, %v", err) + } + + req.Header.Set("FooInnerSpace", " inner space ") + req.Header.Set("FooLeadingSpace", " leading-space") + req.Header.Add("FooMultipleSpace", "no-space") + req.Header.Add("FooMultipleSpace", "\ttab-space") + req.Header.Add("FooMultipleSpace", "trailing-space ") + req.Header.Set("FooNoSpace", "no-space") + req.Header.Set("FooTabSpace", "\ttab-space\t") + req.Header.Set("FooTrailingSpace", "trailing-space ") + req.Header.Set("FooWrappedSpace", " wrapped-space ") + + ctx := &httpSigner{ + ServiceName: serviceName, + Region: region, + Request: req, + Time: v4Internal.NewSigningTime(time.Date(2021, 10, 20, 12, 42, 0, 0, time.UTC)), + KeyDerivator: v4Internal.NewSigningKeyDeriver(), + } + + build, err := ctx.Build() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expectCanonicalString := strings.Join([]string{ + `POST`, + `/`, + ``, + `fooinnerspace:inner space`, + `fooleadingspace:leading-space`, + `foomultiplespace:no-space,tab-space,trailing-space`, + `foonospace:no-space`, + `footabspace:tab-space`, + `footrailingspace:trailing-space`, + `foowrappedspace:wrapped-space`, + `host:mockAPI.mock-region.amazonaws.com`, + `x-amz-date:20211020T124200Z`, + ``, + `fooinnerspace;fooleadingspace;foomultiplespace;foonospace;footabspace;footrailingspace;foowrappedspace;host;x-amz-date`, + ``, + }, "\n") + if diff := cmpDiff(expectCanonicalString, build.CanonicalString); diff != "" { + t.Errorf("expect match, got\n%s", diff) + } +} + +func BenchmarkPresignRequest(b *testing.B) { + signer := NewSigner() + req, bodyHash := buildRequest("dynamodb", "us-east-1", "{}") + + query := req.URL.Query() + query.Set("X-Amz-Expires", "5") + req.URL.RawQuery = query.Encode() + + for i := 0; i < b.N; i++ { + signer.PresignHTTP(context.Background(), testCredentials, req, bodyHash, "dynamodb", "us-east-1", time.Now()) + } +} + +func BenchmarkSignRequest(b *testing.B) { + signer := NewSigner() + req, bodyHash := buildRequest("dynamodb", "us-east-1", "{}") + for i := 0; i < b.N; i++ { + signer.SignHTTP(context.Background(), testCredentials, req, bodyHash, "dynamodb", "us-east-1", time.Now()) + } +} + +func cmpDiff(e, a interface{}) string { + if !reflect.DeepEqual(e, a) { + return fmt.Sprintf("%v != %v", e, a) + } + return "" +} diff --git a/api/handler/put.go b/api/handler/put.go index d08389f..5d99743 100644 --- a/api/handler/put.go +++ b/api/handler/put.go @@ -312,7 +312,8 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) { } func (h *handler) getBodyReader(r *http.Request) (io.ReadCloser, error) { - if !api.IsSignedStreamingV4(r) { + shaType, streaming := api.IsSignedStreamingV4(r) + if !streaming { return r.Body, nil } @@ -345,7 +346,16 @@ func (h *handler) getBodyReader(r *http.Request) (io.ReadCloser, error) { return nil, fmt.Errorf("%w: parse decoded content length: %s", apierr.GetAPIError(apierr.ErrMissingContentLength), err.Error()) } - chunkReader, err := newSignV4ChunkedReader(r) + var ( + err error + chunkReader io.ReadCloser + ) + if shaType == api.StreamingContentV4aSHA256 { + chunkReader, err = newSignV4aChunkedReader(r) + } else { + chunkReader, err = newSignV4ChunkedReader(r) + } + if err != nil { return nil, fmt.Errorf("initialize chunk reader: %w", err) } diff --git a/api/handler/s3v4aReader.go b/api/handler/s3v4aReader.go new file mode 100644 index 0000000..75d3491 --- /dev/null +++ b/api/handler/s3v4aReader.go @@ -0,0 +1,209 @@ +package handler + +import ( + "bufio" + "bytes" + "encoding/hex" + "fmt" + "io" + "net/http" + "time" + + v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2" + errs "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" + credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials" +) + +type ( + s3v4aChunkReader struct { + reader *bufio.Reader + streamSigner *v4a.StreamSigner + + requestTime time.Time + buffer []byte + offset int + err error + } +) + +func (c *s3v4aChunkReader) Close() (err error) { + return nil +} + +func (c *s3v4aChunkReader) Read(buf []byte) (num int, err error) { + if c.offset > 0 { + num = copy(buf, c.buffer[c.offset:]) + if num == len(buf) { + c.offset += num + return num, nil + } + c.offset = 0 + buf = buf[num:] + } + + var size int + for { + b, err := c.reader.ReadByte() + if err != nil { + return c.handleErr(num, err) + } + if b == ';' { // separating character + break + } + + // Manually deserialize the size since AWS specified + // the chunk size to be of variable width. In particular, + // a size of 16 is encoded as `10` while a size of 64 KB + // is `10000`. + switch { + case b >= '0' && b <= '9': + size = size<<4 | int(b-'0') + case b >= 'a' && b <= 'f': + size = size<<4 | int(b-('a'-10)) + case b >= 'A' && b <= 'F': + size = size<<4 | int(b-('A'-10)) + default: + c.err = errMalformedChunkedEncoding + return num, c.err + } + if size > maxChunkSize { + c.err = errGiantChunk + return num, c.err + } + } + + // Now, we read the signature of the following payload and expect: + // chunk-signature=" + + "**\r\n" + // + // The signature is 64 bytes long (hex-encoded SHA256 hash) and + // starts with a 16 byte header: len("chunk-signature=") + 144 == 160. + var signature [160]byte + _, err = io.ReadFull(c.reader, signature[:]) + if err != nil { + return c.handleErr(num, err) + } + if !bytes.HasPrefix(signature[:], []byte(chunkSignatureHeader)) { + c.err = errMalformedChunkedEncoding + return num, c.err + } + b, err := c.reader.ReadByte() + if err != nil { + return c.handleErr(num, err) + } + if b != '\r' { + c.err = errMalformedChunkedEncoding + return num, c.err + } + b, err = c.reader.ReadByte() + if err != nil { + return c.handleErr(num, err) + } + if b != '\n' { + c.err = errMalformedChunkedEncoding + return num, c.err + } + + if cap(c.buffer) < size { + c.buffer = make([]byte, size) + } else { + c.buffer = c.buffer[:size] + } + + // Now, we read the payload and compute its SHA-256 hash. + _, err = io.ReadFull(c.reader, c.buffer) + if err == io.EOF && size != 0 { + err = io.ErrUnexpectedEOF + } + if err != nil && err != io.EOF { + c.err = err + return num, c.err + } + b, err = c.reader.ReadByte() + if b != '\r' || err != nil { + c.err = errMalformedChunkedEncoding + return num, c.err + } + b, err = c.reader.ReadByte() + if err != nil { + return c.handleErr(num, err) + } + if b != '\n' { + c.err = errMalformedChunkedEncoding + return num, c.err + } + + // Once we have read the entire chunk successfully, we verify + // that the received signature is valid. + + n, err := hex.Decode(signature[:], bytes.TrimSuffix(signature[:], []byte("**"))[16:]) + if err != nil { + c.err = errMalformedChunkedEncoding + return num, c.err + } + + if err = c.streamSigner.VerifySignature(nil, c.buffer, c.requestTime, signature[:n]); err != nil { + c.err = err + return num, c.err + } + + // If the chunk size is zero we return io.EOF. As specified by AWS, + // only the last chunk is zero-sized. + if size == 0 { + c.err = io.EOF + return num, c.err + } + + c.offset = copy(buf, c.buffer) + num += c.offset + return num, err +} + +func (c *s3v4aChunkReader) handleErr(num int, err error) (int, error) { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + + c.err = err + return num, c.err +} + +func newSignV4aChunkedReader(req *http.Request) (io.ReadCloser, error) { + box, err := middleware.GetBoxData(req.Context()) + if err != nil { + return nil, errs.GetAPIError(errs.ErrAuthorizationHeaderMalformed) + } + + authHeaders, err := middleware.GetAuthHeaders(req.Context()) + if err != nil { + return nil, errs.GetAPIError(errs.ErrAuthorizationHeaderMalformed) + } + + seed, err := hex.DecodeString(authHeaders.SignatureV4) + if err != nil { + return nil, errs.GetAPIError(errs.ErrSignatureDoesNotMatch) + } + + reqTime, err := middleware.GetClientTime(req.Context()) + if err != nil { + return nil, errs.GetAPIError(errs.ErrMalformedDate) + } + + credAdapter := v4a.SymmetricCredentialAdaptor{ + SymmetricProvider: credentialsv2.NewStaticCredentialsProvider(authHeaders.AccessKeyID, box.Gate.SecretKey, ""), + } + + creds, err := credAdapter.RetrievePrivateKey(req.Context()) + if err != nil { + return nil, fmt.Errorf("failed to derive assymetric key from credentials: %w", err) + } + + newStreamSigner := v4a.NewStreamSigner(creds, "s3", seed) + + return &s3v4aChunkReader{ + reader: bufio.NewReader(req.Body), + streamSigner: newStreamSigner, + requestTime: reqTime, + buffer: make([]byte, 64*1024), + }, nil +} diff --git a/api/headers.go b/api/headers.go index 540d3cc..bfcb295 100644 --- a/api/headers.go +++ b/api/headers.go @@ -94,7 +94,8 @@ const ( DefaultLocationConstraint = "default" - StreamingContentSHA256 = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD" + StreamingContentSHA256 = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD" + StreamingContentV4aSHA256 = "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD" DefaultStorageClass = "STANDARD" ) @@ -125,7 +126,9 @@ var SystemMetadata = map[string]struct{}{ ContentLanguage: {}, } -func IsSignedStreamingV4(r *http.Request) bool { - return r.Header.Get(AmzContentSha256) == StreamingContentSHA256 && - r.Method == http.MethodPut +func IsSignedStreamingV4(r *http.Request) (string, bool) { + shaHeader := r.Header.Get(AmzContentSha256) + return shaHeader, + (shaHeader == StreamingContentSHA256 || shaHeader == StreamingContentV4aSHA256) && + r.Method == http.MethodPut } diff --git a/go.mod b/go.mod index 7c9fa91..daf8bb9 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/aws/aws-sdk-go v1.44.6 github.com/aws/aws-sdk-go-v2 v1.30.3 github.com/aws/aws-sdk-go-v2/credentials v1.17.27 + github.com/aws/smithy-go v1.20.1 github.com/bluele/gcache v0.0.2 github.com/go-chi/chi/v5 v5.0.8 github.com/google/uuid v1.6.0 -- 2.45.2 From e366c8662447f5cf6d8e6848e1e57074cee2edee Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Tue, 23 Apr 2024 14:49:34 +0300 Subject: [PATCH 2/8] [#339] sigv4a: Support presign Signed-off-by: Denis Kirillov --- api/auth/center.go | 17 ++++++++--------- api/auth/center_test.go | 6 +++--- api/auth/presign.go | 10 ++++++---- api/auth/presign_test.go | 17 +++++++++++++---- api/auth/signer/v4asdk2/v4a.go | 15 ++++++++++----- .../modules/generate-presigned-url.go | 17 ++++++++++++++++- 6 files changed, 56 insertions(+), 26 deletions(-) diff --git a/api/auth/center.go b/api/auth/center.go index 2f2b8e1..cc60ad3 100644 --- a/api/auth/center.go +++ b/api/auth/center.go @@ -9,7 +9,6 @@ import ( "io" "mime/multipart" "net/http" - "os" "regexp" "strings" "time" @@ -24,7 +23,6 @@ import ( oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/smithy-go/logging" ) var ( @@ -426,8 +424,6 @@ func (c *Center) checkSign(authHeader *AuthHeader, box *accessbox.Box, request * case signaturePreambleSigV4A: signer := v4a.NewSigner(func(options *v4a.SignerOptions) { options.DisableURIPathEscaping = true - options.LogSigning = true - options.Logger = logging.NewStandardLogger(os.Stdout) }) credAdapter := v4a.SymmetricCredentialAdaptor{ @@ -439,13 +435,16 @@ func (c *Center) checkSign(authHeader *AuthHeader, box *accessbox.Box, request * return fmt.Errorf("failed to derive assymetric key from credentials: %w", err) } - if authHeader.IsPresigned { - if err = checkPresignedDate(authHeader, signatureDateTime); err != nil { - return err - } + if !authHeader.IsPresigned { + return signer.VerifySignature(creds, request, authHeader.PayloadHash, authHeader.Service, + 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) default: return fmt.Errorf("invalid preamble: %s", authHeader.Preamble) diff --git a/api/auth/center_test.go b/api/auth/center_test.go index f4e51dd..e217a57 100644 --- a/api/auth/center_test.go +++ b/api/auth/center_test.go @@ -13,8 +13,8 @@ import ( "time" 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" + "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/creds/accessbox" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens" @@ -24,10 +24,9 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" 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/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" "go.uber.org/zap/zaptest" ) @@ -66,6 +65,7 @@ func TestAuthHeaderParse(t *testing.T) { Signature: "2811ccb9e242f41426738fb1f", SignedFields: []string{"host", "x-amz-content-sha256", "x-amz-date"}, Date: "20210809", + Preamble: signaturePreambleSigV4, }, }, { diff --git a/api/auth/presign.go b/api/auth/presign.go index 6d20b30..b7cbded 100644 --- a/api/auth/presign.go +++ b/api/auth/presign.go @@ -39,11 +39,10 @@ func PresignRequest(creds *credentials.Credentials, reqData RequestData, presign return nil, fmt.Errorf("failed to create new request: %w", err) } - req.Header.Set(AmzDate, presignData.SignTime.Format("20060102T150405Z")) - for k, v := range presignData.Headers { - req.Header.Set(k, v) + req.Header.Set(k, v) // maybe we should filter system header (or keep responsibility on caller) } + req.Header.Set(AmzDate, presignData.SignTime.Format("20060102T150405Z")) signer := v4.NewSigner(creds) signer.DisableURIPathEscaping = true @@ -63,8 +62,11 @@ func PresignRequestV4a(credProvider credentialsv2.StaticCredentialsProvider, req 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(ContentTypeHdr, "text/plain") req.Header.Set(AmzExpires, strconv.Itoa(int(presignData.Lifetime.Seconds()))) signer := v4a.NewSigner(func(options *v4a.SignerOptions) { diff --git a/api/auth/presign_test.go b/api/auth/presign_test.go index 27f8084..6432cce 100644 --- a/api/auth/presign_test.go +++ b/api/auth/presign_test.go @@ -75,6 +75,9 @@ func TestCheckSign(t *testing.T) { Region: "spb", Lifetime: 10 * time.Minute, SignTime: time.Now().UTC(), + Headers: map[string]string{ + ContentTypeHdr: "text/plain", + }, } req, err := PresignRequest(awsCreds, reqData, presignData) @@ -120,11 +123,16 @@ func TestCheckSignV4a(t *testing.T) { Region: "spb", Lifetime: 10 * time.Minute, SignTime: time.Now().UTC(), + Headers: map[string]string{ + ContentTypeHdr: "text/plain", + }, } req, err := PresignRequestV4a(awsCreds, reqData, presignData) require.NoError(t, err) + req.Header.Set(ContentTypeHdr, "text/plain") + expBox := &accessbox.Box{ Gate: &accessbox.GateData{ SecretKey: secretKey, @@ -171,9 +179,7 @@ func TestPresignRequestV4a(t *testing.T) { req, err := http.NewRequest("GET", "http://localhost:8084/bucket/object", nil) require.NoError(t, err) - //req.Header.Set(AmzRegionSet, strings.Join(regionSet, ",")) - //req.Header.Set(AmzDate, signingTime.Format("20060102T150405Z")) - //req.Header.Set(AmzAlgorithm, signaturePreambleSigV4A) + req.Header.Set(AmzExpires, "600") presignedURL, hdr, err := signer.PresignHTTP(req.Context(), creds, req, "", service, regionSet, signingTime) require.NoError(t, err) @@ -185,7 +191,10 @@ func TestPresignRequestV4a(t *testing.T) { r, err := http.NewRequest("GET", presignedURL, nil) 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) } diff --git a/api/auth/signer/v4asdk2/v4a.go b/api/auth/signer/v4asdk2/v4a.go index 305fbd1..5f79859 100644 --- a/api/auth/signer/v4asdk2/v4a.go +++ b/api/auth/signer/v4asdk2/v4a.go @@ -202,6 +202,15 @@ func (s *Signer) SignHTTP(ctx context.Context, credentials Credentials, r *http. // 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 { + 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 for _, fn := range optFns { fn(&options) @@ -214,6 +223,7 @@ func (s *Signer) VerifySignature(credentials Credentials, r *http.Request, paylo RegionSet: regionSet, Credentials: credentials, Time: signingTime.UTC(), + IsPreSign: isPresigned, DisableHeaderHoisting: options.DisableHeaderHoisting, DisableURIPathEscaping: options.DisableURIPathEscaping, } @@ -465,11 +475,6 @@ func (s *httpSigner) buildCanonicalHeaders(host string, rule v4Internal.Rule, he continue // ignored header } - if strings.EqualFold(k, contentLengthHeader) { - // prevent signing already handled content-length header. - continue - } - lowerCaseKey := strings.ToLower(k) if _, ok := signed[lowerCaseKey]; ok { // include additional values diff --git a/cmd/s3-authmate/modules/generate-presigned-url.go b/cmd/s3-authmate/modules/generate-presigned-url.go index 764187c..f29d631 100644 --- a/cmd/s3-authmate/modules/generate-presigned-url.go +++ b/cmd/s3-authmate/modules/generate-presigned-url.go @@ -3,11 +3,13 @@ package modules import ( "encoding/json" "fmt" + "net/http" "os" "strings" "time" "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/credentials" "github.com/aws/aws-sdk-go/aws/session" @@ -38,6 +40,7 @@ const ( awsAccessKeyIDFlag = "aws-access-key-id" awsSecretAccessKeyFlag = "aws-secret-access-key" headerFlag = "header" + sigV4AFlag = "sigv4a" ) func initGeneratePresignedURLCmd() { @@ -51,6 +54,7 @@ func initGeneratePresignedURLCmd() { 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().StringSlice(headerFlag, nil, "Header in form of 'Key: value' to use in presigned URL (use flags repeatedly for multiple headers or separate them by comma)") + generatePresignedURLCmd.Flags().Bool(sigV4AFlag, false, "Use SigV4A for signing request") _ = generatePresignedURLCmd.MarkFlagRequired(endpointFlag) _ = generatePresignedURLCmd.MarkFlagRequired(bucketFlag) @@ -101,7 +105,18 @@ func runGeneratePresignedURLCmd(*cobra.Command, []string) error { } presignData.Headers = headers - 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 { return wrapBusinessLogicError(err) } -- 2.45.2 From 7aad1b6e4234e6deae2c022d954a551675397d49 Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Wed, 4 Sep 2024 16:47:53 +0300 Subject: [PATCH 3/8] [#339] Don't include additional content-length header for signing Signed-off-by: Denis Kirillov --- api/auth/center_test.go | 49 +++++++++++++++++++++------------- api/auth/signer/v4asdk2/v4a.go | 11 ++++---- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/api/auth/center_test.go b/api/auth/center_test.go index e217a57..5b5559c 100644 --- a/api/auth/center_test.go +++ b/api/auth/center_test.go @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "os" "strings" "testing" "time" @@ -26,6 +27,7 @@ import ( 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/smithy-go/logging" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" @@ -94,37 +96,48 @@ func TestSignature(t *testing.T) { } func TestSignatureV4A(t *testing.T) { - var accessKeyAddr oid.Address - err := accessKeyAddr.DecodeString("8N7CYBY74kxZXoyvA5UNdmovaXqFpwNfvEPsqaN81es2/3tDwq5tR8fByrJcyJwyiuYX7Dae8tyDT7pd8oaL1MBto") - require.NoError(t, err) - - accessKeyID := strings.ReplaceAll(accessKeyAddr.String(), "/", "0") - secretKey := "713d0a0b9efc7d22923e17b0402a6a89b4273bc711c8bacb2da1b643d0006aeb" + accessKeyID := "2XEbqH4M3ym7a3E3esxfZ2gRLnMwDXrCN4y1SkQg5fHa09sThVmVL3EE6xeKsyMzaqu5jPi41YCaVbnwbwCTF3bx1" + secretKey := "00637f53f842573aaa06c2164c598973cd986880987111416cf71f1619def537" signer := v4a.NewSigner(func(options *v4a.SignerOptions) { options.DisableURIPathEscaping = true + options.Logger = logging.NewStandardLogger(os.Stdout) + options.LogSigning = true }) credAdapter := v4a.SymmetricCredentialAdaptor{ SymmetricProvider: credentialsv2.NewStaticCredentialsProvider(accessKeyID, secretKey, ""), } - req, err := http.NewRequest("GET", "http://localhost:8084/bucket/object", nil) - require.NoError(t, err) + bodyStr := ` +1b;chunk-signature=3045022100b63692a1b20759bdabd342011823427a8952df75c93174d98ad043abca8052e002201695228a91ba986171b8d0ad20856d3d94ca3614d0a90a50a531ba8e52447b9b** +Testing with the {sdk-java} +0;chunk-signature=30440220455885a2d4e9f705256ca6b0a5a22f7f784780ccbd1c0a371e5db3059c91745b022073259dd44746cbd63261d628a04d25be5a32a974c077c5c2d83c8157fb323b9f**** +` + body := bytes.NewBufferString(bodyStr) + + req, err := http.NewRequest("PUT", "http://localhost:8084/test/tmp", body) + require.NoError(t, err) + req.Header.Set("Amz-Sdk-Invocation-Id", "ca3a3cde-7d26-fce6-ed9c-82f7a0573824") + req.Header.Set("Amz-Sdk-Request", "attempt=2; max=2") + req.Header.Set("Authorization", "AWS4-ECDSA-P256-SHA256 Credential=2XEbqH4M3ym7a3E3esxfZ2gRLnMwDXrCN4y1SkQg5fHa09sThVmVL3EE6xeKsyMzaqu5jPi41YCaVbnwbwCTF3bx1/20240904/s3/aws4_request, SignedHeaders=amz-sdk-invocation-id;amz-sdk-request;content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length;x-amz-region-set, Signature=30440220574244c5ff5deba388c4e3b0541a42113179b6839b3e6b4212d255a118fa9089022056f7b9b72c93f67dbcd25fe9ca67950b5913fc00bb7a62bc276c21e828c0b6c7") + req.Header.Set("Content-Length", "360") + req.Header.Set("Content-Type", "text/plain; charset=UTF-8") + req.Header.Set("X-Amz-Content-Sha256", "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD") + req.Header.Set("X-Amz-Date", "20240904T133253Z") + req.Header.Set("X-Amz-Decoded-Content-Length", "27") + req.Header.Set("X-Amz-Region-Set", "us-east-1") + + service := "s3" + regionSet := []string{"us-east-1"} + signature := "30440220574244c5ff5deba388c4e3b0541a42113179b6839b3e6b4212d255a118fa9089022056f7b9b72c93f67dbcd25fe9ca67950b5913fc00bb7a62bc276c21e828c0b6c7" + signingTime, err := time.Parse("20060102T150405Z", "20240904T133253Z") + require.NoError(t, err) creds, err := credAdapter.RetrievePrivateKey(req.Context()) require.NoError(t, err) - signingTime := time.Now() - service := "s3" - regionSet := []string{"spb"} - - err = signer.SignHTTP(req.Context(), creds, req, "", service, regionSet, signingTime) - require.NoError(t, err) - - signature := NewRegexpMatcher(authorizationFieldV4aRegexp).GetSubmatches(req.Header.Get(AuthorizationHdr))["v4_signature"] - - err = signer.VerifySignature(creds, req, "", service, regionSet, signingTime, signature) + err = signer.VerifySignature(creds, req, "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD", service, regionSet, signingTime, signature) require.NoError(t, err) } diff --git a/api/auth/signer/v4asdk2/v4a.go b/api/auth/signer/v4asdk2/v4a.go index 5f79859..f257ab5 100644 --- a/api/auth/signer/v4asdk2/v4a.go +++ b/api/auth/signer/v4asdk2/v4a.go @@ -16,7 +16,6 @@ import ( "net/textproto" "net/url" "sort" - "strconv" "strings" "time" @@ -464,11 +463,11 @@ func (s *httpSigner) buildCanonicalHeaders(host string, rule v4Internal.Rule, he headers = append(headers, hostHeader) signed[hostHeader] = append(signed[hostHeader], host) - const contentLengthHeader = "content-length" - if length > 0 { - headers = append(headers, contentLengthHeader) - signed[contentLengthHeader] = append(signed[contentLengthHeader], strconv.FormatInt(length, 10)) - } + //const contentLengthHeader = "content-length" + //if length > 0 { + // headers = append(headers, contentLengthHeader) + // signed[contentLengthHeader] = append(signed[contentLengthHeader], strconv.FormatInt(length, 10)) + //} for k, v := range header { if !rule.IsValid(k) { -- 2.45.2 From 7b0a9e9573804cfe4ea68b3649b14541227faa49 Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Wed, 4 Sep 2024 17:35:39 +0300 Subject: [PATCH 4/8] [#339] Drop aws-sdk-go v1 Signed-off-by: Denis Kirillov --- api/auth/center.go | 67 +- api/auth/center_fuzz_test.go | 10 +- api/auth/center_test.go | 63 +- api/auth/presign.go | 35 +- api/auth/presign_test.go | 13 +- api/auth/signer/v4/header_rules.go | 87 -- api/auth/signer/v4/options.go | 7 - api/auth/signer/v4/request_context_go1.7.go | 14 - api/auth/signer/v4/stream.go | 63 -- api/auth/signer/v4/uri_path.go | 25 - api/auth/signer/v4/v4.go | 858 ------------------ .../signer/v4sdk2/signer/v4/middleware.go | 442 --------- .../v4sdk2/signer/v4/presign_middleware.go | 126 --- api/handler/delete_test.go | 21 +- api/handler/put_test.go | 10 +- api/handler/s3reader.go | 20 +- .../modules/generate-presigned-url.go | 68 +- cmd/s3-authmate/modules/sign.go | 88 +- go.mod | 19 +- go.sum | 43 +- 20 files changed, 278 insertions(+), 1801 deletions(-) delete mode 100644 api/auth/signer/v4/header_rules.go delete mode 100644 api/auth/signer/v4/options.go delete mode 100644 api/auth/signer/v4/request_context_go1.7.go delete mode 100644 api/auth/signer/v4/stream.go delete mode 100644 api/auth/signer/v4/uri_path.go delete mode 100644 api/auth/signer/v4/v4.go delete mode 100644 api/auth/signer/v4sdk2/signer/v4/middleware.go delete mode 100644 api/auth/signer/v4sdk2/signer/v4/presign_middleware.go diff --git a/api/auth/center.go b/api/auth/center.go index cc60ad3..1dbf239 100644 --- a/api/auth/center.go +++ b/api/auth/center.go @@ -2,27 +2,30 @@ package auth import ( "context" + "crypto" "crypto/hmac" + "crypto/rand" "crypto/sha256" "encoding/hex" "fmt" "io" "mime/multipart" "net/http" + "net/url" "regexp" "strings" "time" - v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4" - apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2" + v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/v4" + apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" - credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials" - "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/credentials" ) var ( @@ -184,10 +187,11 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) { Date: creds[1], IsPresigned: true, Preamble: signaturePreambleSigV4, + PayloadHash: r.Header.Get(AmzContentSHA256), } authHdr.Expiration, err = time.ParseDuration(queryValues.Get(AmzExpires) + "s") if err != nil { - return nil, fmt.Errorf("couldn't parse X-Amz-Expires: %w", err) + return nil, fmt.Errorf("%w: couldn't parse X-Amz-Expires %v", apierr.GetAPIError(apierr.ErrMalformedExpires), err) } signatureDateTimeStr = queryValues.Get(AmzDate) } else if queryValues.Get(AmzAlgorithm) == signaturePreambleSigV4A { @@ -204,10 +208,11 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) { Date: creds[1], IsPresigned: true, Preamble: signaturePreambleSigV4A, + PayloadHash: r.Header.Get(AmzContentSHA256), } authHdr.Expiration, err = time.ParseDuration(queryValues.Get(AmzExpires) + "s") if err != nil { - return nil, fmt.Errorf("couldn't parse X-Amz-Expires: %w", err) + return nil, fmt.Errorf("%w: couldn't parse X-Amz-Expires %v", apierr.GetAPIError(apierr.ErrMalformedExpires), err) } signatureDateTimeStr = queryValues.Get(AmzDate) } else { @@ -250,7 +255,7 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) { } clonedRequest := cloneRequest(r, authHdr) - if err = c.checkSign(authHdr, box, clonedRequest, signatureDateTime); err != nil { + if err = c.checkSign(r.Context(), authHdr, box, clonedRequest, signatureDateTime); err != nil { return nil, err } @@ -392,26 +397,36 @@ func cloneRequest(r *http.Request, authHeader *AuthHeader) *http.Request { return otherRequest } -func (c *Center) checkSign(authHeader *AuthHeader, box *accessbox.Box, request *http.Request, signatureDateTime time.Time) error { +func (c *Center) checkSign(ctx context.Context, authHeader *AuthHeader, box *accessbox.Box, request *http.Request, signatureDateTime time.Time) error { var signature string switch authHeader.Preamble { case signaturePreambleSigV4: - awsCreds := credentials.NewStaticCredentials(authHeader.AccessKeyID, box.Gate.SecretKey, "") - signer := v4.NewSigner(awsCreds) - signer.DisableURIPathEscaping = true + creds := aws.Credentials{ + AccessKeyID: authHeader.AccessKeyID, + SecretAccessKey: box.Gate.SecretKey, + } + signer := v4.NewSigner(func(options *v4.SignerOptions) { + options.DisableURIPathEscaping = true + }) if authHeader.IsPresigned { if err := checkPresignedDate(authHeader, signatureDateTime); err != nil { return err } - if _, err := signer.Presign(request, nil, authHeader.Service, authHeader.Region, authHeader.Expiration, signatureDateTime); err != nil { + signedURI, _, err := signer.PresignHTTP(ctx, creds, request, authHeader.PayloadHash, authHeader.Service, authHeader.Region, signatureDateTime) + if err != nil { return fmt.Errorf("failed to pre-sign temporary HTTP request: %w", err) } - signature = request.URL.Query().Get(AmzSignature) + + u, err := url.ParseRequestURI(signedURI) + if err != nil { + return err + } + signature = u.Query().Get(AmzSignature) } else { - if _, err := signer.Sign(request, nil, authHeader.Service, authHeader.Region, signatureDateTime); err != nil { + if err := signer.SignHTTP(ctx, creds, request, authHeader.PayloadHash, authHeader.Service, authHeader.Region, signatureDateTime); err != nil { return fmt.Errorf("failed to sign temporary HTTP request: %w", err) } signature = c.reg.GetSubmatches(request.Header.Get(AuthorizationHdr))["v4_signature"] @@ -427,7 +442,7 @@ func (c *Center) checkSign(authHeader *AuthHeader, box *accessbox.Box, request * }) credAdapter := v4a.SymmetricCredentialAdaptor{ - SymmetricProvider: credentialsv2.NewStaticCredentialsProvider(authHeader.AccessKeyID, box.Gate.SecretKey, ""), + SymmetricProvider: credentials.NewStaticCredentialsProvider(authHeader.AccessKeyID, box.Gate.SecretKey, ""), } creds, err := credAdapter.RetrievePrivateKey(request.Context()) @@ -472,6 +487,28 @@ func SignStr(secret, service, region string, t time.Time, strToSign string) stri return hex.EncodeToString(signature) } +func SignStrV4A(cred aws.Credentials, strToSign string) (string, error) { + hash := sha256.New() + hash.Write([]byte(strToSign)) + + credAdapter := v4a.SymmetricCredentialAdaptor{ + SymmetricProvider: credentials.NewStaticCredentialsProvider(cred.AccessKeyID, cred.SecretAccessKey, ""), + } + + creds, err := credAdapter.RetrievePrivateKey(context.Background()) // because of using StaticCredentialsProvider + if err != nil { + // no error is expected + panic(err) + } + + sig, err := creds.PrivateKey.Sign(rand.Reader, hash.Sum(nil), crypto.SHA256) + if err != nil { + return "", err + } + + return hex.EncodeToString(sig), nil +} + func deriveKey(secret, service, region string, t time.Time) []byte { hmacDate := hmacSHA256([]byte("AWS4"+secret), []byte(t.UTC().Format("20060102"))) hmacRegion := hmacSHA256(hmacDate, []byte(region)) diff --git a/api/auth/center_fuzz_test.go b/api/auth/center_fuzz_test.go index 36099af..a5247da 100644 --- a/api/auth/center_fuzz_test.go +++ b/api/auth/center_fuzz_test.go @@ -4,13 +4,14 @@ package auth import ( + "context" "strings" "testing" "time" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" - "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go-v2/aws" utils "github.com/trailofbits/go-fuzz-utils" ) @@ -41,7 +42,10 @@ func DoFuzzAuthenticate(input []byte) int { accessKeyID := strings.ReplaceAll(accessKeyAddr.String(), "/", "0") secretKey, err := tp.GetString() - awsCreds := credentials.NewStaticCredentials(accessKeyID, secretKey, "") + if err != nil { + return fuzzFailExitCode + } + awsCreds := aws.Credentials{AccessKeyID: accessKeyID, SecretAccessKey: secretKey} reqData := RequestData{ Method: "GET", @@ -56,7 +60,7 @@ func DoFuzzAuthenticate(input []byte) int { SignTime: time.Now().UTC(), } - req, err := PresignRequest(awsCreds, reqData, presignData) + req, err := PresignRequest(context.Background(), awsCreds, reqData, presignData) if req == nil { return fuzzFailExitCode } diff --git a/api/auth/center_test.go b/api/auth/center_test.go index 5b5559c..23e7d1c 100644 --- a/api/auth/center_test.go +++ b/api/auth/center_test.go @@ -13,8 +13,8 @@ import ( "testing" "time" - v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4" v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2" + v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/v4" "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/creds/accessbox" @@ -25,8 +25,8 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" 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-v2/aws" + "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/smithy-go/logging" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/stretchr/testify/require" @@ -106,7 +106,7 @@ func TestSignatureV4A(t *testing.T) { }) credAdapter := v4a.SymmetricCredentialAdaptor{ - SymmetricProvider: credentialsv2.NewStaticCredentialsProvider(accessKeyID, secretKey, ""), + SymmetricProvider: credentials.NewStaticCredentialsProvider(accessKeyID, secretKey, ""), } bodyStr := ` @@ -216,6 +216,7 @@ func (f *frostFSMock) CreateObject(context.Context, tokens.PrmObjectCreate) (oid } func TestAuthenticate(t *testing.T) { + ctx := context.Background() key, err := keys.NewPrivateKey() require.NoError(t, err) @@ -246,8 +247,8 @@ func TestAuthenticate(t *testing.T) { frostfs := newFrostFSMock() frostfs.objects[accessKeyID] = &obj - awsCreds := credentials.NewStaticCredentials(accessKeyID, secret.SecretKey, "") - defaultSigner := v4.NewSigner(awsCreds) + awsCreds := aws.Credentials{AccessKeyID: accessKeyID, SecretAccessKey: secret.SecretKey} + defaultSigner := v4.NewSigner() service, region := "s3", "default" invalidValue := "invalid-value" @@ -270,7 +271,7 @@ func TestAuthenticate(t *testing.T) { prefixes: []string{addr.Container().String()}, request: func() *http.Request { r := httptest.NewRequest(http.MethodPost, "/", nil) - _, err = defaultSigner.Sign(r, nil, service, region, time.Now()) + err = defaultSigner.SignHTTP(ctx, awsCreds, r, "", service, region, time.Now()) require.NoError(t, err) return r }(), @@ -296,8 +297,8 @@ func TestAuthenticate(t *testing.T) { name: "invalid access key id format", request: func() *http.Request { r := httptest.NewRequest(http.MethodPost, "/", nil) - signer := v4.NewSigner(credentials.NewStaticCredentials(addr.Object().String(), secret.SecretKey, "")) - _, err = signer.Sign(r, nil, service, region, time.Now()) + cred := aws.Credentials{AccessKeyID: addr.Object().String(), SecretAccessKey: secret.SecretKey} + err = v4.NewSigner().SignHTTP(ctx, cred, r, "", service, region, time.Now()) require.NoError(t, err) return r }(), @@ -309,7 +310,7 @@ func TestAuthenticate(t *testing.T) { prefixes: []string{addr.Object().String()}, request: func() *http.Request { r := httptest.NewRequest(http.MethodPost, "/", nil) - _, err = defaultSigner.Sign(r, nil, service, region, time.Now()) + err = defaultSigner.SignHTTP(ctx, awsCreds, r, "", service, region, time.Now()) require.NoError(t, err) return r }(), @@ -320,8 +321,8 @@ func TestAuthenticate(t *testing.T) { name: "invalid access key id value", request: func() *http.Request { r := httptest.NewRequest(http.MethodPost, "/", nil) - signer := v4.NewSigner(credentials.NewStaticCredentials(accessKeyID[:len(accessKeyID)-4], secret.SecretKey, "")) - _, err = signer.Sign(r, nil, service, region, time.Now()) + cred := aws.Credentials{AccessKeyID: accessKeyID[:len(accessKeyID)-4], SecretAccessKey: secret.SecretKey} + err = v4.NewSigner().SignHTTP(ctx, cred, r, "", service, region, time.Now()) require.NoError(t, err) return r }(), @@ -332,8 +333,8 @@ func TestAuthenticate(t *testing.T) { name: "unknown access key id", request: func() *http.Request { r := httptest.NewRequest(http.MethodPost, "/", nil) - signer := v4.NewSigner(credentials.NewStaticCredentials(addr.Object().String()+"0"+addr.Container().String(), secret.SecretKey, "")) - _, err = signer.Sign(r, nil, service, region, time.Now()) + cred := aws.Credentials{AccessKeyID: addr.Object().String() + "0" + addr.Container().String(), SecretAccessKey: secret.SecretKey} + err = v4.NewSigner().SignHTTP(ctx, cred, r, "", service, region, time.Now()) require.NoError(t, err) return r }(), @@ -343,8 +344,8 @@ func TestAuthenticate(t *testing.T) { name: "invalid signature", request: func() *http.Request { r := httptest.NewRequest(http.MethodPost, "/", nil) - signer := v4.NewSigner(credentials.NewStaticCredentials(accessKeyID, "secret", "")) - _, err = signer.Sign(r, nil, service, region, time.Now()) + cred := aws.Credentials{AccessKeyID: accessKeyID, SecretAccessKey: "secret"} + err = v4.NewSigner().SignHTTP(ctx, cred, r, "", service, region, time.Now()) require.NoError(t, err) return r }(), @@ -356,7 +357,7 @@ func TestAuthenticate(t *testing.T) { prefixes: []string{addr.Container().String()}, request: func() *http.Request { r := httptest.NewRequest(http.MethodPost, "/", nil) - _, err = defaultSigner.Sign(r, nil, service, region, time.Now()) + err = defaultSigner.SignHTTP(ctx, awsCreds, r, "", service, region, time.Now()) r.Header.Set(AmzDate, invalidValue) require.NoError(t, err) return r @@ -368,7 +369,7 @@ func TestAuthenticate(t *testing.T) { prefixes: []string{addr.Container().String()}, request: func() *http.Request { r := httptest.NewRequest(http.MethodPost, "/", nil) - _, err = defaultSigner.Sign(r, nil, service, region, time.Now()) + err = defaultSigner.SignHTTP(ctx, awsCreds, r, "", service, region, time.Now()) r.Header.Set(AmzContentSHA256, invalidValue) require.NoError(t, err) return r @@ -379,7 +380,10 @@ func TestAuthenticate(t *testing.T) { name: "valid presign", request: func() *http.Request { r := httptest.NewRequest(http.MethodPost, "/", nil) - _, err = defaultSigner.Presign(r, nil, service, region, time.Minute, time.Now()) + r.Header.Set(AmzExpires, "60") + signedURI, _, err := defaultSigner.PresignHTTP(ctx, awsCreds, r, "", service, region, time.Now()) + require.NoError(t, err) + r.URL, err = url.ParseRequestURI(signedURI) require.NoError(t, err) return r }(), @@ -401,20 +405,24 @@ func TestAuthenticate(t *testing.T) { name: "presign, bad X-Amz-Expires", request: func() *http.Request { r := httptest.NewRequest(http.MethodPost, "/", nil) - _, err = defaultSigner.Presign(r, nil, service, region, time.Minute, time.Now()) - queryParams := r.URL.Query() - queryParams.Set("X-Amz-Expires", invalidValue) - r.URL.RawQuery = queryParams.Encode() + r.Header.Set(AmzExpires, invalidValue) + signedURI, _, err := defaultSigner.PresignHTTP(ctx, awsCreds, r, UnsignedPayload, service, region, time.Now()) + require.NoError(t, err) + r.URL, err = url.ParseRequestURI(signedURI) require.NoError(t, err) return r }(), - err: true, + err: true, + errCode: errors.ErrMalformedExpires, }, { name: "presign, expired", request: func() *http.Request { r := httptest.NewRequest(http.MethodPost, "/", nil) - _, err = defaultSigner.Presign(r, nil, service, region, time.Minute, time.Now().Add(-time.Minute)) + r.Header.Set(AmzExpires, "60") + signedURI, _, err := defaultSigner.PresignHTTP(ctx, awsCreds, r, UnsignedPayload, service, region, time.Now().Add(-time.Minute)) + require.NoError(t, err) + r.URL, err = url.ParseRequestURI(signedURI) require.NoError(t, err) return r }(), @@ -425,7 +433,10 @@ func TestAuthenticate(t *testing.T) { name: "presign, signature from future", request: func() *http.Request { r := httptest.NewRequest(http.MethodPost, "/", nil) - _, err = defaultSigner.Presign(r, nil, service, region, time.Minute, time.Now().Add(time.Minute)) + r.Header.Set(AmzExpires, "60") + signedURI, _, err := defaultSigner.PresignHTTP(ctx, awsCreds, r, UnsignedPayload, service, region, time.Now().Add(time.Minute)) + require.NoError(t, err) + r.URL, err = url.ParseRequestURI(signedURI) require.NoError(t, err) return r }(), diff --git a/api/auth/presign.go b/api/auth/presign.go index b7cbded..8d0f2d3 100644 --- a/api/auth/presign.go +++ b/api/auth/presign.go @@ -1,18 +1,20 @@ package auth import ( + "context" "fmt" "net/http" + "net/url" "os" "strconv" "strings" "time" - v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4" v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2" - credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/private/protocol/rest" + v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/v4" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/smithy-go/encoding/httpbinding" "github.com/aws/smithy-go/logging" ) @@ -32,8 +34,8 @@ type PresignData struct { } // PresignRequest forms pre-signed request to access objects without aws credentials. -func PresignRequest(creds *credentials.Credentials, reqData RequestData, presignData PresignData) (*http.Request, error) { - urlStr := fmt.Sprintf("%s/%s/%s", reqData.Endpoint, rest.EscapePath(reqData.Bucket, false), rest.EscapePath(reqData.Object, false)) +func PresignRequest(ctx context.Context, creds aws.Credentials, reqData RequestData, presignData PresignData) (*http.Request, error) { + urlStr := fmt.Sprintf("%s/%s/%s", reqData.Endpoint, httpbinding.EscapePath(reqData.Bucket, false), httpbinding.EscapePath(reqData.Object, false)) req, err := http.NewRequest(strings.ToUpper(reqData.Method), urlStr, nil) if err != nil { return nil, fmt.Errorf("failed to create new request: %w", err) @@ -43,20 +45,27 @@ func PresignRequest(creds *credentials.Credentials, reqData RequestData, presign 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(AmzExpires, strconv.FormatFloat(presignData.Lifetime.Round(time.Second).Seconds(), 'f', 0, 64)) - signer := v4.NewSigner(creds) - signer.DisableURIPathEscaping = true + signer := v4.NewSigner(func(options *v4.SignerOptions) { + options.DisableURIPathEscaping = true + }) - if _, err = signer.Presign(req, nil, presignData.Service, presignData.Region, presignData.Lifetime, presignData.SignTime); err != nil { + signedURI, _, err := signer.PresignHTTP(ctx, creds, req, presignData.Headers[AmzContentSHA256], presignData.Service, presignData.Region, presignData.SignTime) + if err != nil { return nil, fmt.Errorf("presign: %w", err) } + if req.URL, err = url.ParseRequestURI(signedURI); err != nil { + return nil, fmt.Errorf("parse signed URI: %w", err) + } + return req, nil } // PresignRequestV4a forms pre-signed request to access objects without aws credentials. -func PresignRequestV4a(credProvider credentialsv2.StaticCredentialsProvider, reqData RequestData, presignData PresignData) (*http.Request, error) { - urlStr := fmt.Sprintf("%s/%s/%s", reqData.Endpoint, rest.EscapePath(reqData.Bucket, false), rest.EscapePath(reqData.Object, false)) +func PresignRequestV4a(cred aws.Credentials, reqData RequestData, presignData PresignData) (*http.Request, error) { + urlStr := fmt.Sprintf("%s/%s/%s", reqData.Endpoint, httpbinding.EscapePath(reqData.Bucket, false), httpbinding.EscapePath(reqData.Object, false)) req, err := http.NewRequest(strings.ToUpper(reqData.Method), urlStr, nil) if err != nil { return nil, fmt.Errorf("failed to create new request: %w", err) @@ -76,14 +85,14 @@ func PresignRequestV4a(credProvider credentialsv2.StaticCredentialsProvider, req }) credAdapter := v4a.SymmetricCredentialAdaptor{ - SymmetricProvider: credProvider, + SymmetricProvider: credentials.NewStaticCredentialsProvider(cred.AccessKeyID, cred.SecretAccessKey, ""), } creds, err := credAdapter.RetrievePrivateKey(req.Context()) if err != nil { return nil, fmt.Errorf("failed to derive assymetric key from credentials: %w", err) } - presignedURL, _, err := signer.PresignHTTP(req.Context(), creds, req, "", presignData.Service, []string{presignData.Region}, presignData.SignTime) + presignedURL, _, err := signer.PresignHTTP(req.Context(), creds, req, presignData.Headers[AmzContentSHA256], presignData.Service, []string{presignData.Region}, presignData.SignTime) if err != nil { return nil, fmt.Errorf("presign: %w", err) } diff --git a/api/auth/presign_test.go b/api/auth/presign_test.go index 6432cce..c91499b 100644 --- a/api/auth/presign_test.go +++ b/api/auth/presign_test.go @@ -16,8 +16,8 @@ import ( cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" + "github.com/aws/aws-sdk-go-v2/aws" credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials" - "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/smithy-go/logging" "github.com/stretchr/testify/require" ) @@ -56,13 +56,15 @@ func (m credentialsMock) Update(context.Context, tokens.CredentialsParam) (oid.A } func TestCheckSign(t *testing.T) { + ctx := context.Background() + var accessKeyAddr oid.Address err := accessKeyAddr.DecodeString("8N7CYBY74kxZXoyvA5UNdmovaXqFpwNfvEPsqaN81es2/3tDwq5tR8fByrJcyJwyiuYX7Dae8tyDT7pd8oaL1MBto") require.NoError(t, err) accessKeyID := strings.ReplaceAll(accessKeyAddr.String(), "/", "0") secretKey := "713d0a0b9efc7d22923e17b0402a6a89b4273bc711c8bacb2da1b643d0006aeb" - awsCreds := credentials.NewStaticCredentials(accessKeyID, secretKey, "") + awsCreds := aws.Credentials{AccessKeyID: accessKeyID, SecretAccessKey: secretKey} reqData := RequestData{ Method: "GET", @@ -76,11 +78,12 @@ func TestCheckSign(t *testing.T) { Lifetime: 10 * time.Minute, SignTime: time.Now().UTC(), Headers: map[string]string{ - ContentTypeHdr: "text/plain", + ContentTypeHdr: "text/plain", + AmzContentSHA256: UnsignedPayload, }, } - req, err := PresignRequest(awsCreds, reqData, presignData) + req, err := PresignRequest(ctx, awsCreds, reqData, presignData) require.NoError(t, err) expBox := &accessbox.Box{ @@ -110,7 +113,7 @@ func TestCheckSignV4a(t *testing.T) { accessKeyID := strings.ReplaceAll(accessKeyAddr.String(), "/", "0") secretKey := "713d0a0b9efc7d22923e17b0402a6a89b4273bc711c8bacb2da1b643d0006aeb" - awsCreds := credentialsv2.NewStaticCredentialsProvider(accessKeyID, secretKey, "") + awsCreds := aws.Credentials{AccessKeyID: accessKeyID, SecretAccessKey: secretKey} reqData := RequestData{ Method: "GET", diff --git a/api/auth/signer/v4/header_rules.go b/api/auth/signer/v4/header_rules.go deleted file mode 100644 index f2c0b44..0000000 --- a/api/auth/signer/v4/header_rules.go +++ /dev/null @@ -1,87 +0,0 @@ -package v4 - -import ( - "strings" -) - -// validator houses a set of rule needed for validation of a -// string value. -type rules []rule - -// rule interface allows for more flexible rules and just simply -// checks whether or not a value adheres to that rule. -type rule interface { - IsValid(value string) bool -} - -// IsValid will iterate through all rules and see if any rules -// apply to the value and supports nested rules. -func (r rules) IsValid(value string) bool { - for _, rule := range r { - if rule.IsValid(value) { - return true - } - } - return false -} - -// mapRule generic rule for maps. -type mapRule map[string]struct{} - -// IsValid for the map rule satisfies whether it exists in the map. -func (m mapRule) IsValid(value string) bool { - _, ok := m[value] - return ok -} - -// whitelist is a generic rule for whitelisting. -type whitelist struct { - rule -} - -// IsValid for whitelist checks if the value is within the whitelist. -func (w whitelist) IsValid(value string) bool { - return w.rule.IsValid(value) -} - -// blacklist is a generic rule for blacklisting. -type blacklist struct { - rule -} - -// IsValid for whitelist checks if the value is within the whitelist. -func (b blacklist) IsValid(value string) bool { - return !b.rule.IsValid(value) -} - -type patterns []string - -// IsValid for patterns checks each pattern and returns if a match has -// been found. -func (p patterns) IsValid(value string) bool { - for _, pattern := range p { - if HasPrefixFold(value, pattern) { - return true - } - } - return false -} - -// HasPrefixFold tests whether the string s begins with prefix, interpreted as UTF-8 strings, -// under Unicode case-folding. -func HasPrefixFold(s, prefix string) bool { - return len(s) >= len(prefix) && strings.EqualFold(s[0:len(prefix)], prefix) -} - -// inclusiveRules rules allow for rules to depend on one another. -type inclusiveRules []rule - -// IsValid will return true if all rules are true. -func (r inclusiveRules) IsValid(value string) bool { - for _, rule := range r { - if !rule.IsValid(value) { - return false - } - } - return true -} diff --git a/api/auth/signer/v4/options.go b/api/auth/signer/v4/options.go deleted file mode 100644 index 6aa2ed2..0000000 --- a/api/auth/signer/v4/options.go +++ /dev/null @@ -1,7 +0,0 @@ -package v4 - -// WithUnsignedPayload will enable and set the UnsignedPayload field to -// true of the signer. -func WithUnsignedPayload(v4 *Signer) { - v4.UnsignedPayload = true -} diff --git a/api/auth/signer/v4/request_context_go1.7.go b/api/auth/signer/v4/request_context_go1.7.go deleted file mode 100644 index 21fe74e..0000000 --- a/api/auth/signer/v4/request_context_go1.7.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build go1.7 -// +build go1.7 - -package v4 - -import ( - "net/http" - - "github.com/aws/aws-sdk-go/aws" -) - -func requestContext(r *http.Request) aws.Context { - return r.Context() -} diff --git a/api/auth/signer/v4/stream.go b/api/auth/signer/v4/stream.go deleted file mode 100644 index eb579a8..0000000 --- a/api/auth/signer/v4/stream.go +++ /dev/null @@ -1,63 +0,0 @@ -package v4 - -import ( - "encoding/hex" - "strings" - "time" - - "github.com/aws/aws-sdk-go/aws/credentials" -) - -type credentialValueProvider interface { - Get() (credentials.Value, error) -} - -// StreamSigner implements signing of event stream encoded payloads. -type StreamSigner struct { - region string - service string - - credentials credentialValueProvider - - prevSig []byte -} - -// NewStreamSigner creates a SigV4 signer used to sign Event Stream encoded messages. -func NewStreamSigner(region, service string, seedSignature []byte, credentials *credentials.Credentials) *StreamSigner { - return &StreamSigner{ - region: region, - service: service, - credentials: credentials, - prevSig: seedSignature, - } -} - -// GetSignature takes an event stream encoded headers and payload and returns a signature. -func (s *StreamSigner) GetSignature(headers, payload []byte, date time.Time) ([]byte, error) { - credValue, err := s.credentials.Get() - if err != nil { - return nil, err - } - - sigKey := deriveSigningKey(s.region, s.service, credValue.SecretAccessKey, date) - - keyPath := buildSigningScope(s.region, s.service, date) - - stringToSign := buildEventStreamStringToSign(headers, payload, s.prevSig, keyPath, date) - - signature := hmacSHA256(sigKey, []byte(stringToSign)) - s.prevSig = signature - - return signature, nil -} - -func buildEventStreamStringToSign(headers, payload, prevSig []byte, scope string, date time.Time) string { - return strings.Join([]string{ - "AWS4-HMAC-SHA256-PAYLOAD", - formatTime(date), - scope, - hex.EncodeToString(prevSig), - hex.EncodeToString(hashSHA256(headers)), - hex.EncodeToString(hashSHA256(payload)), - }, "\n") -} diff --git a/api/auth/signer/v4/uri_path.go b/api/auth/signer/v4/uri_path.go deleted file mode 100644 index 7711ec7..0000000 --- a/api/auth/signer/v4/uri_path.go +++ /dev/null @@ -1,25 +0,0 @@ -//go:build go1.5 -// +build go1.5 - -package v4 - -import ( - "net/url" - "strings" -) - -func getURIPath(u *url.URL) string { - var uri string - - if len(u.Opaque) > 0 { - uri = "/" + strings.Join(strings.Split(u.Opaque, "/")[3:], "/") - } else { - uri = u.EscapedPath() - } - - if len(uri) == 0 { - uri = "/" - } - - return uri -} diff --git a/api/auth/signer/v4/v4.go b/api/auth/signer/v4/v4.go deleted file mode 100644 index 94afc34..0000000 --- a/api/auth/signer/v4/v4.go +++ /dev/null @@ -1,858 +0,0 @@ -// Package v4 implements signing for AWS V4 signer -// -// Provides request signing for request that need to be signed with -// AWS V4 Signatures. -// -// # Standalone Signer -// -// Generally using the signer outside of the SDK should not require any additional -// logic when using Go v1.5 or higher. The signer does this by taking advantage -// of the URL.EscapedPath method. If your request URI requires additional escaping -// you many need to use the URL.Opaque to define what the raw URI should be sent -// to the service as. -// -// The signer will first check the URL.Opaque field, and use its value if set. -// The signer does require the URL.Opaque field to be set in the form of: -// -// "///" -// -// // e.g. -// "//example.com/some/path" -// -// The leading "//" and hostname are required or the URL.Opaque escaping will -// not work correctly. -// -// If URL.Opaque is not set the signer will fallback to the URL.EscapedPath() -// method and using the returned value. If you're using Go v1.4 you must set -// URL.Opaque if the URI path needs escaping. If URL.Opaque is not set with -// Go v1.5 the signer will fallback to URL.Path. -// -// AWS v4 signature validation requires that the canonical string's URI path -// element must be the URI escaped form of the HTTP request's path. -// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html -// -// The Go HTTP client will perform escaping automatically on the request. Some -// of these escaping may cause signature validation errors because the HTTP -// request differs from the URI path or query that the signature was generated. -// https://golang.org/pkg/net/url/#URL.EscapedPath -// -// Because of this, it is recommended that when using the signer outside of the -// SDK that explicitly escaping the request prior to being signed is preferable, -// and will help prevent signature validation errors. This can be done by setting -// the URL.Opaque or URL.RawPath. The SDK will use URL.Opaque first and then -// call URL.EscapedPath() if Opaque is not set. -// -// If signing a request intended for HTTP2 server, and you're using Go 1.6.2 -// through 1.7.4 you should use the URL.RawPath as the pre-escaped form of the -// request URL. https://github.com/golang/go/issues/16847 points to a bug in -// Go pre 1.8 that fails to make HTTP2 requests using absolute URL in the HTTP -// message. URL.Opaque generally will force Go to make requests with absolute URL. -// URL.RawPath does not do this, but RawPath must be a valid escaping of Path -// or url.EscapedPath will ignore the RawPath escaping. -// -// Test `TestStandaloneSign` provides a complete example of using the signer -// outside of the SDK and pre-escaping the URI path. -package v4 - -import ( - "crypto/hmac" - "crypto/sha256" - "encoding/hex" - "fmt" - "io" - "net/http" - "net/url" - "sort" - "strconv" - "strings" - "time" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/private/protocol/rest" -) - -const ( - authorizationHeader = "Authorization" - authHeaderSignatureElem = "Signature=" - signatureQueryKey = "X-Amz-Signature" - - authHeaderPrefix = "AWS4-HMAC-SHA256" - timeFormat = "20060102T150405Z" - shortTimeFormat = "20060102" - awsV4Request = "aws4_request" - - // emptyStringSHA256 is a SHA256 of an empty string. - emptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` -) - -var ignoredPresignHeaders = rules{ - blacklist{ - mapRule{ - authorizationHeader: struct{}{}, - "User-Agent": struct{}{}, - "X-Amzn-Trace-Id": struct{}{}, - }, - }, -} - -// drop User-Agent header to be compatible with aws sdk java v1. -var ignoredHeaders = rules{ - blacklist{ - mapRule{ - authorizationHeader: struct{}{}, - "X-Amzn-Trace-Id": struct{}{}, - }, - }, -} - -// requiredSignedHeaders is a whitelist for build canonical headers. -var requiredSignedHeaders = rules{ - whitelist{ - mapRule{ - "Cache-Control": struct{}{}, - "Content-Disposition": struct{}{}, - "Content-Encoding": struct{}{}, - "Content-Language": struct{}{}, - "Content-Md5": struct{}{}, - "Content-Type": struct{}{}, - "Expires": struct{}{}, - "If-Match": struct{}{}, - "If-Modified-Since": struct{}{}, - "If-None-Match": struct{}{}, - "If-Unmodified-Since": struct{}{}, - "Range": struct{}{}, - "X-Amz-Acl": struct{}{}, - "X-Amz-Copy-Source": struct{}{}, - "X-Amz-Copy-Source-If-Match": struct{}{}, - "X-Amz-Copy-Source-If-Modified-Since": struct{}{}, - "X-Amz-Copy-Source-If-None-Match": struct{}{}, - "X-Amz-Copy-Source-If-Unmodified-Since": struct{}{}, - "X-Amz-Copy-Source-Range": struct{}{}, - "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": struct{}{}, - "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": struct{}{}, - "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": struct{}{}, - "X-Amz-Grant-Full-control": struct{}{}, - "X-Amz-Grant-Read": struct{}{}, - "X-Amz-Grant-Read-Acp": struct{}{}, - "X-Amz-Grant-Write": struct{}{}, - "X-Amz-Grant-Write-Acp": struct{}{}, - "X-Amz-Metadata-Directive": struct{}{}, - "X-Amz-Mfa": struct{}{}, - "X-Amz-Request-Payer": struct{}{}, - "X-Amz-Server-Side-Encryption": struct{}{}, - "X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": struct{}{}, - "X-Amz-Server-Side-Encryption-Customer-Algorithm": struct{}{}, - "X-Amz-Server-Side-Encryption-Customer-Key": struct{}{}, - "X-Amz-Server-Side-Encryption-Customer-Key-Md5": struct{}{}, - "X-Amz-Storage-Class": struct{}{}, - "X-Amz-Tagging": struct{}{}, - "X-Amz-Website-Redirect-Location": struct{}{}, - "X-Amz-Content-Sha256": struct{}{}, - }, - }, - patterns{"X-Amz-Meta-"}, -} - -// allowedHoisting is a whitelist for build query headers. The boolean value -// represents whether or not it is a pattern. -var allowedQueryHoisting = inclusiveRules{ - blacklist{requiredSignedHeaders}, - patterns{"X-Amz-"}, -} - -// Signer applies AWS v4 signing to given request. Use this to sign requests -// that need to be signed with AWS V4 Signatures. -type Signer struct { - // The authentication credentials the request will be signed against. - // This value must be set to sign requests. - Credentials *credentials.Credentials - - // Sets the log level the signer should use when reporting information to - // the logger. If the logger is nil nothing will be logged. See - // aws.LogLevelType for more information on available logging levels - // - // By default nothing will be logged. - Debug aws.LogLevelType - - // The logger loging information will be written to. If there the logger - // is nil, nothing will be logged. - Logger aws.Logger - - // Disables the Signer's moving HTTP header key/value pairs from the HTTP - // request header to the request's query string. This is most commonly used - // with pre-signed requests preventing headers from being added to the - // request's query string. - DisableHeaderHoisting bool - - // Disables the automatic escaping of the URI path of the request for the - // siganture's canonical string's path. For services that do not need additional - // escaping then use this to disable the signer escaping the path. - // - // S3 is an example of a service that does not need additional escaping. - // - // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html - DisableURIPathEscaping bool - - // Disables the automatical setting of the HTTP request's Body field with the - // io.ReadSeeker passed in to the signer. This is useful if you're using a - // custom wrapper around the body for the io.ReadSeeker and want to preserve - // the Body value on the Request.Body. - // - // This does run the risk of signing a request with a body that will not be - // sent in the request. Need to ensure that the underlying data of the Body - // values are the same. - DisableRequestBodyOverwrite bool - - // currentTimeFn returns the time value which represents the current time. - // This value should only be used for testing. If it is nil the default - // time.Now will be used. - currentTimeFn func() time.Time - - // UnsignedPayload will prevent signing of the payload. This will only - // work for services that have support for this. - UnsignedPayload bool -} - -// NewSigner returns a Signer pointer configured with the credentials and optional -// option values provided. If not options are provided the Signer will use its -// default configuration. -func NewSigner(credentials *credentials.Credentials, options ...func(*Signer)) *Signer { - v4 := &Signer{ - Credentials: credentials, - } - - for _, option := range options { - option(v4) - } - - return v4 -} - -type signingCtx struct { - ServiceName string - Region string - Request *http.Request - Body io.ReadSeeker - Query url.Values - Time time.Time - ExpireTime time.Duration - SignedHeaderVals http.Header - - DisableURIPathEscaping bool - - credValues credentials.Value - isPresign bool - unsignedPayload bool - - bodyDigest string - signedHeaders string - canonicalHeaders string - canonicalString string - credentialString string - stringToSign string - signature string -} - -// Sign signs AWS v4 requests with the provided body, service name, region the -// request is made to, and time the request is signed at. The signTime allows -// you to specify that a request is signed for the future, and cannot be -// used until then. -// -// Returns a list of HTTP headers that were included in the signature or an -// error if signing the request failed. Generally for signed requests this value -// is not needed as the full request context will be captured by the http.Request -// value. It is included for reference though. -// -// Sign will set the request's Body to be the `body` parameter passed in. If -// the body is not already an io.ReadCloser, it will be wrapped within one. If -// a `nil` body parameter passed to Sign, the request's Body field will be -// also set to nil. Its important to note that this functionality will not -// change the request's ContentLength of the request. -// -// Sign differs from Presign in that it will sign the request using HTTP -// header values. This type of signing is intended for http.Request values that -// will not be shared, or are shared in a way the header values on the request -// will not be lost. -// -// The requests body is an io.ReadSeeker so the SHA256 of the body can be -// generated. To bypass the signer computing the hash you can set the -// "X-Amz-Content-Sha256" header with a precomputed value. The signer will -// only compute the hash if the request header value is empty. -func (v4 Signer) Sign(r *http.Request, body io.ReadSeeker, service, region string, signTime time.Time) (http.Header, error) { - return v4.signWithBody(r, body, service, region, 0, false, signTime) -} - -// Presign signs AWS v4 requests with the provided body, service name, region -// the request is made to, and time the request is signed at. The signTime -// allows you to specify that a request is signed for the future, and cannot -// be used until then. -// -// Returns a list of HTTP headers that were included in the signature or an -// error if signing the request failed. For presigned requests these headers -// and their values must be included on the HTTP request when it is made. This -// is helpful to know what header values need to be shared with the party the -// presigned request will be distributed to. -// -// Presign differs from Sign in that it will sign the request using query string -// instead of header values. This allows you to share the Presigned Request's -// URL with third parties, or distribute it throughout your system with minimal -// dependencies. -// -// Presign also takes an exp value which is the duration the -// signed request will be valid after the signing time. This is allows you to -// set when the request will expire. -// -// The requests body is an io.ReadSeeker so the SHA256 of the body can be -// generated. To bypass the signer computing the hash you can set the -// "X-Amz-Content-Sha256" header with a precomputed value. The signer will -// only compute the hash if the request header value is empty. -// -// Presigning a S3 request will not compute the body's SHA256 hash by default. -// This is done due to the general use case for S3 presigned URLs is to share -// PUT/GET capabilities. If you would like to include the body's SHA256 in the -// presigned request's signature you can set the "X-Amz-Content-Sha256" -// HTTP header and that will be included in the request's signature. -func (v4 Signer) Presign(r *http.Request, body io.ReadSeeker, service, region string, exp time.Duration, signTime time.Time) (http.Header, error) { - return v4.signWithBody(r, body, service, region, exp, true, signTime) -} - -func (v4 Signer) signWithBody(r *http.Request, body io.ReadSeeker, service, region string, exp time.Duration, isPresign bool, signTime time.Time) (http.Header, error) { - currentTimeFn := v4.currentTimeFn - if currentTimeFn == nil { - currentTimeFn = time.Now - } - - ctx := &signingCtx{ - Request: r, - Body: body, - Query: r.URL.Query(), - Time: signTime, - ExpireTime: exp, - isPresign: isPresign, - ServiceName: service, - Region: region, - DisableURIPathEscaping: v4.DisableURIPathEscaping, - unsignedPayload: v4.UnsignedPayload, - } - - for key := range ctx.Query { - sort.Strings(ctx.Query[key]) - } - - if ctx.isRequestSigned() { - ctx.Time = currentTimeFn() - ctx.handlePresignRemoval() - } - - var err error - ctx.credValues, err = v4.Credentials.GetWithContext(requestContext(r)) - if err != nil { - return http.Header{}, err - } - - ctx.sanitizeHostForHeader() - ctx.assignAmzQueryValues() - if err := ctx.build(v4.DisableHeaderHoisting); err != nil { - return nil, err - } - - // If the request is not presigned the body should be attached to it. This - // prevents the confusion of wanting to send a signed request without - // the body the request was signed for attached. - if !(v4.DisableRequestBodyOverwrite || ctx.isPresign) { - var reader io.ReadCloser - if body != nil { - var ok bool - if reader, ok = body.(io.ReadCloser); !ok { - reader = io.NopCloser(body) - } - } - r.Body = reader - } - - if v4.Debug.Matches(aws.LogDebugWithSigning) { - v4.logSigningInfo(ctx) - } - - return ctx.SignedHeaderVals, nil -} - -func (ctx *signingCtx) sanitizeHostForHeader() { - request.SanitizeHostForHeader(ctx.Request) -} - -func (ctx *signingCtx) handlePresignRemoval() { - if !ctx.isPresign { - return - } - - // The credentials have expired for this request. The current signing - // is invalid, and needs to be request because the request will fail. - ctx.removePresign() - - // Update the request's query string to ensure the values stays in - // sync in the case retrieving the new credentials fails. - ctx.Request.URL.RawQuery = ctx.Query.Encode() -} - -func (ctx *signingCtx) assignAmzQueryValues() { - if ctx.isPresign { - ctx.Query.Set("X-Amz-Algorithm", authHeaderPrefix) - if ctx.credValues.SessionToken != "" { - ctx.Query.Set("X-Amz-Security-Token", ctx.credValues.SessionToken) - } else { - ctx.Query.Del("X-Amz-Security-Token") - } - - return - } - - if ctx.credValues.SessionToken != "" { - ctx.Request.Header.Set("X-Amz-Security-Token", ctx.credValues.SessionToken) - } -} - -// SignRequestHandler is a named request handler the SDK will use to sign -// service client request with using the V4 signature. -var SignRequestHandler = request.NamedHandler{ - Name: "v4.SignRequestHandler", Fn: SignSDKRequest, -} - -// SignSDKRequest signs an AWS request with the V4 signature. This -// request handler should only be used with the SDK's built in service client's -// API operation requests. -// -// This function should not be used on its on its own, but in conjunction with -// an AWS service client's API operation call. To sign a standalone request -// not created by a service client's API operation method use the "Sign" or -// "Presign" functions of the "Signer" type. -// -// If the credentials of the request's config are set to -// credentials.AnonymousCredentials the request will not be signed. -func SignSDKRequest(req *request.Request) { - SignSDKRequestWithCurrentTime(req, time.Now) -} - -// BuildNamedHandler will build a generic handler for signing. -func BuildNamedHandler(name string, opts ...func(*Signer)) request.NamedHandler { - return request.NamedHandler{ - Name: name, - Fn: func(req *request.Request) { - SignSDKRequestWithCurrentTime(req, time.Now, opts...) - }, - } -} - -// SignSDKRequestWithCurrentTime will sign the SDK's request using the time -// function passed in. Behaves the same as SignSDKRequest with the exception -// the request is signed with the value returned by the current time function. -func SignSDKRequestWithCurrentTime(req *request.Request, curTimeFn func() time.Time, opts ...func(*Signer)) { - // If the request does not need to be signed ignore the signing of the - // request if the AnonymousCredentials object is used. - if req.Config.Credentials == credentials.AnonymousCredentials { - return - } - - region := req.ClientInfo.SigningRegion - if region == "" { - region = aws.StringValue(req.Config.Region) - } - - name := req.ClientInfo.SigningName - if name == "" { - name = req.ClientInfo.ServiceName - } - - v4 := NewSigner(req.Config.Credentials, func(v4 *Signer) { - v4.Debug = req.Config.LogLevel.Value() - v4.Logger = req.Config.Logger - v4.DisableHeaderHoisting = req.NotHoist - v4.currentTimeFn = curTimeFn - if name == "s3" { - // S3 service should not have any escaping applied - v4.DisableURIPathEscaping = true - } - // Prevents setting the HTTPRequest's Body. Since the Body could be - // wrapped in a custom io.Closer that we do not want to be stompped - // on top of by the signer. - v4.DisableRequestBodyOverwrite = true - }) - - for _, opt := range opts { - opt(v4) - } - - curTime := curTimeFn() - signedHeaders, err := v4.signWithBody(req.HTTPRequest, req.GetBody(), - name, region, req.ExpireTime, req.ExpireTime > 0, curTime, - ) - if err != nil { - req.Error = err - req.SignedHeaderVals = nil - return - } - - req.SignedHeaderVals = signedHeaders - req.LastSignedAt = curTime -} - -const logSignInfoMsg = `DEBUG: Request Signature: ----[ CANONICAL STRING ]----------------------------- -%s ----[ STRING TO SIGN ]-------------------------------- -%s%s ------------------------------------------------------` -const logSignedURLMsg = ` ----[ SIGNED URL ]------------------------------------ -%s` - -func (v4 *Signer) logSigningInfo(ctx *signingCtx) { - signedURLMsg := "" - if ctx.isPresign { - signedURLMsg = fmt.Sprintf(logSignedURLMsg, ctx.Request.URL.String()) - } - msg := fmt.Sprintf(logSignInfoMsg, ctx.canonicalString, ctx.stringToSign, signedURLMsg) - v4.Logger.Log(msg) -} - -func (ctx *signingCtx) build(disableHeaderHoisting bool) error { - ctx.buildTime() // no depends - ctx.buildCredentialString() // no depends - - if err := ctx.buildBodyDigest(); err != nil { - return err - } - - unsignedHeaders := ctx.Request.Header - if ctx.isPresign { - if !disableHeaderHoisting { - var urlValues url.Values - urlValues, unsignedHeaders = buildQuery(allowedQueryHoisting, unsignedHeaders) // no depends - for k := range urlValues { - ctx.Query[k] = urlValues[k] - } - } - } - - if ctx.isPresign { - ctx.buildCanonicalHeaders(ignoredPresignHeaders, unsignedHeaders) - } else { - ctx.buildCanonicalHeaders(ignoredHeaders, unsignedHeaders) - } - ctx.buildCanonicalString() // depends on canon headers / signed headers - ctx.buildStringToSign() // depends on canon string - ctx.buildSignature() // depends on string to sign - - if ctx.isPresign { - ctx.Request.URL.RawQuery += "&" + signatureQueryKey + "=" + ctx.signature - } else { - parts := []string{ - authHeaderPrefix + " Credential=" + ctx.credValues.AccessKeyID + "/" + ctx.credentialString, - "SignedHeaders=" + ctx.signedHeaders, - authHeaderSignatureElem + ctx.signature, - } - ctx.Request.Header.Set(authorizationHeader, strings.Join(parts, ", ")) - } - - return nil -} - -// GetSignedRequestSignature attempts to extract the signature of the request. -// Returning an error if the request is unsigned, or unable to extract the -// signature. -func GetSignedRequestSignature(r *http.Request) ([]byte, error) { - if auth := r.Header.Get(authorizationHeader); len(auth) != 0 { - ps := strings.Split(auth, ", ") - for _, p := range ps { - if idx := strings.Index(p, authHeaderSignatureElem); idx >= 0 { - sig := p[len(authHeaderSignatureElem):] - if len(sig) == 0 { - return nil, fmt.Errorf("invalid request signature authorization header") - } - return hex.DecodeString(sig) - } - } - } - - if sig := r.URL.Query().Get("X-Amz-Signature"); len(sig) != 0 { - return hex.DecodeString(sig) - } - - return nil, fmt.Errorf("request not signed") -} - -func (ctx *signingCtx) buildTime() { - if ctx.isPresign { - duration := int64(ctx.ExpireTime / time.Second) - ctx.Query.Set("X-Amz-Date", formatTime(ctx.Time)) - ctx.Query.Set("X-Amz-Expires", strconv.FormatInt(duration, 10)) - } else { - ctx.Request.Header.Set("X-Amz-Date", formatTime(ctx.Time)) - } -} - -func (ctx *signingCtx) buildCredentialString() { - ctx.credentialString = buildSigningScope(ctx.Region, ctx.ServiceName, ctx.Time) - - if ctx.isPresign { - ctx.Query.Set("X-Amz-Credential", ctx.credValues.AccessKeyID+"/"+ctx.credentialString) - } -} - -func buildQuery(r rule, header http.Header) (url.Values, http.Header) { - query := url.Values{} - unsignedHeaders := http.Header{} - for k, h := range header { - if r.IsValid(k) { - query[k] = h - } else { - unsignedHeaders[k] = h - } - } - - return query, unsignedHeaders -} -func (ctx *signingCtx) buildCanonicalHeaders(r rule, header http.Header) { - var headers []string - headers = append(headers, "host") - for k, v := range header { - if !r.IsValid(k) { - continue // ignored header - } - if ctx.SignedHeaderVals == nil { - ctx.SignedHeaderVals = make(http.Header) - } - - lowerCaseKey := strings.ToLower(k) - if _, ok := ctx.SignedHeaderVals[lowerCaseKey]; ok { - // include additional values - ctx.SignedHeaderVals[lowerCaseKey] = append(ctx.SignedHeaderVals[lowerCaseKey], v...) - continue - } - - headers = append(headers, lowerCaseKey) - ctx.SignedHeaderVals[lowerCaseKey] = v - } - sort.Strings(headers) - - ctx.signedHeaders = strings.Join(headers, ";") - - if ctx.isPresign { - ctx.Query.Set("X-Amz-SignedHeaders", ctx.signedHeaders) - } - - headerValues := make([]string, len(headers)) - for i, k := range headers { - if k == "host" { - if ctx.Request.Host != "" { - headerValues[i] = "host:" + ctx.Request.Host - } else { - headerValues[i] = "host:" + ctx.Request.URL.Host - } - } else { - headerValues[i] = k + ":" + - strings.Join(ctx.SignedHeaderVals[k], ",") - } - } - stripExcessSpaces(headerValues) - ctx.canonicalHeaders = strings.Join(headerValues, "\n") -} - -func (ctx *signingCtx) buildCanonicalString() { - ctx.Request.URL.RawQuery = strings.Replace(ctx.Query.Encode(), "+", "%20", -1) - - uri := getURIPath(ctx.Request.URL) - - if !ctx.DisableURIPathEscaping { - uri = rest.EscapePath(uri, false) - } - - ctx.canonicalString = strings.Join([]string{ - ctx.Request.Method, - uri, - ctx.Request.URL.RawQuery, - ctx.canonicalHeaders + "\n", - ctx.signedHeaders, - ctx.bodyDigest, - }, "\n") -} - -func (ctx *signingCtx) buildStringToSign() { - ctx.stringToSign = strings.Join([]string{ - authHeaderPrefix, - formatTime(ctx.Time), - ctx.credentialString, - hex.EncodeToString(hashSHA256([]byte(ctx.canonicalString))), - }, "\n") -} - -func (ctx *signingCtx) buildSignature() { - creds := deriveSigningKey(ctx.Region, ctx.ServiceName, ctx.credValues.SecretAccessKey, ctx.Time) - signature := hmacSHA256(creds, []byte(ctx.stringToSign)) - ctx.signature = hex.EncodeToString(signature) -} - -func (ctx *signingCtx) buildBodyDigest() error { - hash := ctx.Request.Header.Get("X-Amz-Content-Sha256") - if hash == "" { - includeSHA256Header := ctx.unsignedPayload || - ctx.ServiceName == "s3" || - ctx.ServiceName == "glacier" - - s3Presign := ctx.isPresign && ctx.ServiceName == "s3" - - if ctx.unsignedPayload || s3Presign { - hash = "UNSIGNED-PAYLOAD" - includeSHA256Header = !s3Presign - } else if ctx.Body == nil { - hash = emptyStringSHA256 - } else { - if !aws.IsReaderSeekable(ctx.Body) { - return fmt.Errorf("cannot use unseekable request body %T, for signed request with body", ctx.Body) - } - hashBytes, err := makeSha256Reader(ctx.Body) - if err != nil { - return err - } - hash = hex.EncodeToString(hashBytes) - } - - if includeSHA256Header { - ctx.Request.Header.Set("X-Amz-Content-Sha256", hash) - } - } - ctx.bodyDigest = hash - - return nil -} - -// isRequestSigned returns if the request is currently signed or presigned. -func (ctx *signingCtx) isRequestSigned() bool { - if ctx.isPresign && ctx.Query.Get("X-Amz-Signature") != "" { - return true - } - if ctx.Request.Header.Get("Authorization") != "" { - return true - } - - return false -} - -// unsign removes signing flags for both signed and presigned requests. -func (ctx *signingCtx) removePresign() { - ctx.Query.Del("X-Amz-Algorithm") - ctx.Query.Del("X-Amz-Signature") - ctx.Query.Del("X-Amz-Security-Token") - ctx.Query.Del("X-Amz-Date") - ctx.Query.Del("X-Amz-Expires") - ctx.Query.Del("X-Amz-Credential") - ctx.Query.Del("X-Amz-SignedHeaders") -} - -func hmacSHA256(key []byte, data []byte) []byte { - hash := hmac.New(sha256.New, key) - hash.Write(data) - return hash.Sum(nil) -} - -func hashSHA256(data []byte) []byte { - hash := sha256.New() - hash.Write(data) - return hash.Sum(nil) -} - -func makeSha256Reader(reader io.ReadSeeker) (hashBytes []byte, err error) { - hash := sha256.New() - start, err := reader.Seek(0, io.SeekCurrent) - if err != nil { - return nil, err - } - defer func() { - // ensure error is return if unable to seek back to start of payload. - _, err = reader.Seek(start, io.SeekStart) - }() - - // Use CopyN to avoid allocating the 32KB buffer in io.Copy for bodies - // smaller than 32KB. Fall back to io.Copy if we fail to determine the size. - size, err := aws.SeekerLen(reader) - if err != nil { - _, _ = io.Copy(hash, reader) - } else { - _, _ = io.CopyN(hash, reader, size) - } - - return hash.Sum(nil), nil -} - -const doubleSpace = " " - -// stripExcessSpaces will rewrite the passed in slice's string values to not -// contain multiple side-by-side spaces. -// -//nolint:revive -func stripExcessSpaces(vals []string) { - var j, k, l, m, spaces int - for i, str := range vals { - // Trim trailing spaces - for j = len(str) - 1; j >= 0 && str[j] == ' '; j-- { - } - - // Trim leading spaces - for k = 0; k < j && str[k] == ' '; k++ { - } - str = str[k : j+1] - - // Strip multiple spaces. - j = strings.Index(str, doubleSpace) - if j < 0 { - vals[i] = str - continue - } - - buf := []byte(str) - for k, m, l = j, j, len(buf); k < l; k++ { - if buf[k] == ' ' { - if spaces == 0 { - // First space. - buf[m] = buf[k] - m++ - } - spaces++ - } else { - // End of multiple spaces. - spaces = 0 - buf[m] = buf[k] - m++ - } - } - - vals[i] = string(buf[:m]) - } -} - -func buildSigningScope(region, service string, dt time.Time) string { - return strings.Join([]string{ - formatShortTime(dt), - region, - service, - awsV4Request, - }, "/") -} - -func deriveSigningKey(region, service, secretKey string, dt time.Time) []byte { - hmacDate := hmacSHA256([]byte("AWS4"+secretKey), []byte(formatShortTime(dt))) - hmacRegion := hmacSHA256(hmacDate, []byte(region)) - hmacService := hmacSHA256(hmacRegion, []byte(service)) - signingKey := hmacSHA256(hmacService, []byte(awsV4Request)) - return signingKey -} - -func formatShortTime(dt time.Time) string { - return dt.UTC().Format(shortTimeFormat) -} - -func formatTime(dt time.Time) string { - return dt.UTC().Format(timeFormat) -} diff --git a/api/auth/signer/v4sdk2/signer/v4/middleware.go b/api/auth/signer/v4sdk2/signer/v4/middleware.go deleted file mode 100644 index 82bf735..0000000 --- a/api/auth/signer/v4sdk2/signer/v4/middleware.go +++ /dev/null @@ -1,442 +0,0 @@ -package v4 - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "fmt" - "io" - "net/http" - "strings" - "time" - - v4Internal "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/internal/v4" - "github.com/aws/aws-sdk-go-v2/aws" - awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" - "github.com/aws/aws-sdk-go-v2/aws/middleware/private/metrics" - "github.com/aws/smithy-go/middleware" - smithyhttp "github.com/aws/smithy-go/transport/http" -) - -const computePayloadHashMiddlewareID = "ComputePayloadHash" - -// HashComputationError indicates an error occurred while computing the signing hash -type HashComputationError struct { - Err error -} - -// Error is the error message -func (e *HashComputationError) Error() string { - return fmt.Sprintf("failed to compute payload hash: %v", e.Err) -} - -// Unwrap returns the underlying error if one is set -func (e *HashComputationError) Unwrap() error { - return e.Err -} - -// SigningError indicates an error condition occurred while performing SigV4 signing -type SigningError struct { - Err error -} - -func (e *SigningError) Error() string { - return fmt.Sprintf("failed to sign request: %v", e.Err) -} - -// Unwrap returns the underlying error cause -func (e *SigningError) Unwrap() error { - return e.Err -} - -// UseDynamicPayloadSigningMiddleware swaps the compute payload sha256 middleware with a resolver middleware that -// switches between unsigned and signed payload based on TLS state for request. -// This middleware should not be used for AWS APIs that do not support unsigned payload signing auth. -// By default, SDK uses this middleware for known AWS APIs that support such TLS based auth selection . -// -// Usage example - -// S3 PutObject API allows unsigned payload signing auth usage when TLS is enabled, and uses this middleware to -// dynamically switch between unsigned and signed payload based on TLS state for request. -func UseDynamicPayloadSigningMiddleware(stack *middleware.Stack) error { - _, err := stack.Finalize.Swap(computePayloadHashMiddlewareID, &dynamicPayloadSigningMiddleware{}) - return err -} - -// dynamicPayloadSigningMiddleware dynamically resolves the middleware that computes and set payload sha256 middleware. -type dynamicPayloadSigningMiddleware struct { -} - -// ID returns the resolver identifier -func (m *dynamicPayloadSigningMiddleware) ID() string { - return computePayloadHashMiddlewareID -} - -// HandleFinalize delegates SHA256 computation according to whether the request -// is TLS-enabled. -func (m *dynamicPayloadSigningMiddleware) HandleFinalize( - ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler, -) ( - out middleware.FinalizeOutput, metadata middleware.Metadata, err error, -) { - req, ok := in.Request.(*smithyhttp.Request) - if !ok { - return out, metadata, fmt.Errorf("unknown transport type %T", in.Request) - } - - if req.IsHTTPS() { - return (&UnsignedPayload{}).HandleFinalize(ctx, in, next) - } - return (&ComputePayloadSHA256{}).HandleFinalize(ctx, in, next) -} - -// UnsignedPayload sets the SigV4 request payload hash to unsigned. -// -// Will not set the Unsigned Payload magic SHA value, if a SHA has already been -// stored in the context. (e.g. application pre-computed SHA256 before making -// API call). -// -// This middleware does not check the X-Amz-Content-Sha256 header, if that -// header is serialized a middleware must translate it into the context. -type UnsignedPayload struct{} - -// AddUnsignedPayloadMiddleware adds unsignedPayload to the operation -// middleware stack -func AddUnsignedPayloadMiddleware(stack *middleware.Stack) error { - return stack.Finalize.Insert(&UnsignedPayload{}, "ResolveEndpointV2", middleware.After) -} - -// ID returns the unsignedPayload identifier -func (m *UnsignedPayload) ID() string { - return computePayloadHashMiddlewareID -} - -// HandleFinalize sets the payload hash magic value to the unsigned sentinel. -func (m *UnsignedPayload) HandleFinalize( - ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler, -) ( - out middleware.FinalizeOutput, metadata middleware.Metadata, err error, -) { - if GetPayloadHash(ctx) == "" { - ctx = SetPayloadHash(ctx, v4Internal.UnsignedPayload) - } - return next.HandleFinalize(ctx, in) -} - -// ComputePayloadSHA256 computes SHA256 payload hash to sign. -// -// Will not set the Unsigned Payload magic SHA value, if a SHA has already been -// stored in the context. (e.g. application pre-computed SHA256 before making -// API call). -// -// This middleware does not check the X-Amz-Content-Sha256 header, if that -// header is serialized a middleware must translate it into the context. -type ComputePayloadSHA256 struct{} - -// AddComputePayloadSHA256Middleware adds computePayloadSHA256 to the -// operation middleware stack -func AddComputePayloadSHA256Middleware(stack *middleware.Stack) error { - return stack.Finalize.Insert(&ComputePayloadSHA256{}, "ResolveEndpointV2", middleware.After) -} - -// RemoveComputePayloadSHA256Middleware removes computePayloadSHA256 from the -// operation middleware stack -func RemoveComputePayloadSHA256Middleware(stack *middleware.Stack) error { - _, err := stack.Finalize.Remove(computePayloadHashMiddlewareID) - return err -} - -// ID is the middleware name -func (m *ComputePayloadSHA256) ID() string { - return computePayloadHashMiddlewareID -} - -// HandleFinalize computes the payload hash for the request, storing it to the -// context. This is a no-op if a caller has previously set that value. -func (m *ComputePayloadSHA256) HandleFinalize( - ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler, -) ( - out middleware.FinalizeOutput, metadata middleware.Metadata, err error, -) { - if GetPayloadHash(ctx) != "" { - return next.HandleFinalize(ctx, in) - } - - req, ok := in.Request.(*smithyhttp.Request) - if !ok { - return out, metadata, &HashComputationError{ - Err: fmt.Errorf("unexpected request middleware type %T", in.Request), - } - } - - hash := sha256.New() - if stream := req.GetStream(); stream != nil { - _, err = io.Copy(hash, stream) - if err != nil { - return out, metadata, &HashComputationError{ - Err: fmt.Errorf("failed to compute payload hash, %w", err), - } - } - - if err := req.RewindStream(); err != nil { - return out, metadata, &HashComputationError{ - Err: fmt.Errorf("failed to seek body to start, %w", err), - } - } - } - - ctx = SetPayloadHash(ctx, hex.EncodeToString(hash.Sum(nil))) - - return next.HandleFinalize(ctx, in) -} - -// SwapComputePayloadSHA256ForUnsignedPayloadMiddleware replaces the -// ComputePayloadSHA256 middleware with the UnsignedPayload middleware. -// -// Use this to disable computing the Payload SHA256 checksum and instead use -// UNSIGNED-PAYLOAD for the SHA256 value. -func SwapComputePayloadSHA256ForUnsignedPayloadMiddleware(stack *middleware.Stack) error { - _, err := stack.Finalize.Swap(computePayloadHashMiddlewareID, &UnsignedPayload{}) - return err -} - -// ContentSHA256Header sets the X-Amz-Content-Sha256 header value to -// the Payload hash stored in the context. -type ContentSHA256Header struct{} - -// AddContentSHA256HeaderMiddleware adds ContentSHA256Header to the -// operation middleware stack -func AddContentSHA256HeaderMiddleware(stack *middleware.Stack) error { - return stack.Finalize.Insert(&ContentSHA256Header{}, computePayloadHashMiddlewareID, middleware.After) -} - -// RemoveContentSHA256HeaderMiddleware removes contentSHA256Header middleware -// from the operation middleware stack -func RemoveContentSHA256HeaderMiddleware(stack *middleware.Stack) error { - _, err := stack.Finalize.Remove((*ContentSHA256Header)(nil).ID()) - return err -} - -// ID returns the ContentSHA256HeaderMiddleware identifier -func (m *ContentSHA256Header) ID() string { - return "SigV4ContentSHA256Header" -} - -// HandleFinalize sets the X-Amz-Content-Sha256 header value to the Payload hash -// stored in the context. -func (m *ContentSHA256Header) HandleFinalize( - ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler, -) ( - out middleware.FinalizeOutput, metadata middleware.Metadata, err error, -) { - req, ok := in.Request.(*smithyhttp.Request) - if !ok { - return out, metadata, &HashComputationError{Err: fmt.Errorf("unexpected request middleware type %T", in.Request)} - } - - req.Header.Set(v4Internal.ContentSHAKey, GetPayloadHash(ctx)) - return next.HandleFinalize(ctx, in) -} - -// SignHTTPRequestMiddlewareOptions is the configuration options for -// [SignHTTPRequestMiddleware]. -// -// Deprecated: [SignHTTPRequestMiddleware] is deprecated. -type SignHTTPRequestMiddlewareOptions struct { - CredentialsProvider aws.CredentialsProvider - Signer HTTPSigner - LogSigning bool -} - -// SignHTTPRequestMiddleware is a `FinalizeMiddleware` implementation for SigV4 -// HTTP Signing. -// -// Deprecated: AWS service clients no longer use this middleware. Signing as an -// SDK operation is now performed through an internal per-service middleware -// which opaquely selects and uses the signer from the resolved auth scheme. -type SignHTTPRequestMiddleware struct { - credentialsProvider aws.CredentialsProvider - signer HTTPSigner - logSigning bool -} - -// NewSignHTTPRequestMiddleware constructs a [SignHTTPRequestMiddleware] using -// the given [Signer] for signing requests. -// -// Deprecated: SignHTTPRequestMiddleware is deprecated. -func NewSignHTTPRequestMiddleware(options SignHTTPRequestMiddlewareOptions) *SignHTTPRequestMiddleware { - return &SignHTTPRequestMiddleware{ - credentialsProvider: options.CredentialsProvider, - signer: options.Signer, - logSigning: options.LogSigning, - } -} - -// ID is the SignHTTPRequestMiddleware identifier. -// -// Deprecated: SignHTTPRequestMiddleware is deprecated. -func (s *SignHTTPRequestMiddleware) ID() string { - return "Signing" -} - -// HandleFinalize will take the provided input and sign the request using the -// SigV4 authentication scheme. -// -// Deprecated: SignHTTPRequestMiddleware is deprecated. -func (s *SignHTTPRequestMiddleware) HandleFinalize(ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler) ( - out middleware.FinalizeOutput, metadata middleware.Metadata, err error, -) { - if !haveCredentialProvider(s.credentialsProvider) { - return next.HandleFinalize(ctx, in) - } - - req, ok := in.Request.(*smithyhttp.Request) - if !ok { - return out, metadata, &SigningError{Err: fmt.Errorf("unexpected request middleware type %T", in.Request)} - } - - signingName, signingRegion := awsmiddleware.GetSigningName(ctx), awsmiddleware.GetSigningRegion(ctx) - payloadHash := GetPayloadHash(ctx) - if len(payloadHash) == 0 { - return out, metadata, &SigningError{Err: fmt.Errorf("computed payload hash missing from context")} - } - - mctx := metrics.Context(ctx) - - if mctx != nil { - if attempt, err := mctx.Data().LatestAttempt(); err == nil { - attempt.CredentialFetchStartTime = time.Now() - } - } - - credentials, err := s.credentialsProvider.Retrieve(ctx) - - if mctx != nil { - if attempt, err := mctx.Data().LatestAttempt(); err == nil { - attempt.CredentialFetchEndTime = time.Now() - } - } - - if err != nil { - return out, metadata, &SigningError{Err: fmt.Errorf("failed to retrieve credentials: %w", err)} - } - - signerOptions := []func(o *SignerOptions){ - func(o *SignerOptions) { - o.Logger = middleware.GetLogger(ctx) - o.LogSigning = s.logSigning - }, - } - - // existing DisableURIPathEscaping is equivalent in purpose - // to authentication scheme property DisableDoubleEncoding - //disableDoubleEncoding, overridden := internalauth.GetDisableDoubleEncoding(ctx) // internalauth "github.com/aws/aws-sdk-go-v2/internal/auth" - //if overridden { - // signerOptions = append(signerOptions, func(o *SignerOptions) { - // o.DisableURIPathEscaping = disableDoubleEncoding - // }) - //} - - if mctx != nil { - if attempt, err := mctx.Data().LatestAttempt(); err == nil { - attempt.SignStartTime = time.Now() - } - } - - err = s.signer.SignHTTP(ctx, credentials, req.Request, payloadHash, signingName, signingRegion, time.Now(), signerOptions...) - - if mctx != nil { - if attempt, err := mctx.Data().LatestAttempt(); err == nil { - attempt.SignEndTime = time.Now() - } - } - - if err != nil { - return out, metadata, &SigningError{Err: fmt.Errorf("failed to sign http request, %w", err)} - } - - ctx = awsmiddleware.SetSigningCredentials(ctx, credentials) - - return next.HandleFinalize(ctx, in) -} - -// StreamingEventsPayload signs input event stream messages. -type StreamingEventsPayload struct{} - -// AddStreamingEventsPayload adds the streamingEventsPayload middleware to the stack. -func AddStreamingEventsPayload(stack *middleware.Stack) error { - return stack.Finalize.Add(&StreamingEventsPayload{}, middleware.Before) -} - -// ID identifies the middleware. -func (s *StreamingEventsPayload) ID() string { - return computePayloadHashMiddlewareID -} - -// HandleFinalize marks the input stream to be signed with SigV4. -func (s *StreamingEventsPayload) HandleFinalize( - ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler, -) ( - out middleware.FinalizeOutput, metadata middleware.Metadata, err error, -) { - contentSHA := GetPayloadHash(ctx) - if len(contentSHA) == 0 { - contentSHA = v4Internal.StreamingEventsPayload - } - - ctx = SetPayloadHash(ctx, contentSHA) - - return next.HandleFinalize(ctx, in) -} - -// GetSignedRequestSignature attempts to extract the signature of the request. -// Returning an error if the request is unsigned, or unable to extract the -// signature. -func GetSignedRequestSignature(r *http.Request) ([]byte, error) { - const authHeaderSignatureElem = "Signature=" - - if auth := r.Header.Get(authorizationHeader); len(auth) != 0 { - ps := strings.Split(auth, ", ") - for _, p := range ps { - if idx := strings.Index(p, authHeaderSignatureElem); idx >= 0 { - sig := p[len(authHeaderSignatureElem):] - if len(sig) == 0 { - return nil, fmt.Errorf("invalid request signature authorization header") - } - return hex.DecodeString(sig) - } - } - } - - if sig := r.URL.Query().Get("X-Amz-Signature"); len(sig) != 0 { - return hex.DecodeString(sig) - } - - return nil, fmt.Errorf("request not signed") -} - -func haveCredentialProvider(p aws.CredentialsProvider) bool { - if p == nil { - return false - } - - return !aws.IsCredentialsProvider(p, (*aws.AnonymousCredentials)(nil)) -} - -type payloadHashKey struct{} - -// GetPayloadHash retrieves the payload hash to use for signing -// -// Scoped to stack values. Use github.com/aws/smithy-go/middleware#ClearStackValues -// to clear all stack values. -func GetPayloadHash(ctx context.Context) (v string) { - v, _ = middleware.GetStackValue(ctx, payloadHashKey{}).(string) - return v -} - -// SetPayloadHash sets the payload hash to be used for signing the request -// -// Scoped to stack values. Use github.com/aws/smithy-go/middleware#ClearStackValues -// to clear all stack values. -func SetPayloadHash(ctx context.Context, hash string) context.Context { - return middleware.WithStackValue(ctx, payloadHashKey{}, hash) -} diff --git a/api/auth/signer/v4sdk2/signer/v4/presign_middleware.go b/api/auth/signer/v4sdk2/signer/v4/presign_middleware.go deleted file mode 100644 index 8522eb3..0000000 --- a/api/auth/signer/v4sdk2/signer/v4/presign_middleware.go +++ /dev/null @@ -1,126 +0,0 @@ -package v4 - -import ( - "context" - "fmt" - "net/http" - "time" - - "github.com/aws/aws-sdk-go-v2/aws" - awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" - "github.com/aws/smithy-go/middleware" - smithyHTTP "github.com/aws/smithy-go/transport/http" -) - -// HTTPPresigner is an interface to a SigV4 signer that can sign create a -// presigned URL for a HTTP requests. -type HTTPPresigner interface { - PresignHTTP( - ctx context.Context, credentials aws.Credentials, r *http.Request, - payloadHash string, service string, region string, signingTime time.Time, - optFns ...func(*SignerOptions), - ) (url string, signedHeader http.Header, err error) -} - -// PresignedHTTPRequest provides the URL and signed headers that are included -// in the presigned URL. -type PresignedHTTPRequest struct { - URL string - Method string - SignedHeader http.Header -} - -// PresignHTTPRequestMiddlewareOptions is the options for the PresignHTTPRequestMiddleware middleware. -type PresignHTTPRequestMiddlewareOptions struct { - CredentialsProvider aws.CredentialsProvider - Presigner HTTPPresigner - LogSigning bool -} - -// PresignHTTPRequestMiddleware provides the Finalize middleware for creating a -// presigned URL for an HTTP request. -// -// Will short circuit the middleware stack and not forward onto the next -// Finalize handler. -type PresignHTTPRequestMiddleware struct { - credentialsProvider aws.CredentialsProvider - presigner HTTPPresigner - logSigning bool -} - -// NewPresignHTTPRequestMiddleware returns a new PresignHTTPRequestMiddleware -// initialized with the presigner. -func NewPresignHTTPRequestMiddleware(options PresignHTTPRequestMiddlewareOptions) *PresignHTTPRequestMiddleware { - return &PresignHTTPRequestMiddleware{ - credentialsProvider: options.CredentialsProvider, - presigner: options.Presigner, - logSigning: options.LogSigning, - } -} - -// ID provides the middleware ID. -func (*PresignHTTPRequestMiddleware) ID() string { return "PresignHTTPRequest" } - -// HandleFinalize will take the provided input and create a presigned url for -// the http request using the SigV4 presign authentication scheme. -// -// Since the signed request is not a valid HTTP request -func (s *PresignHTTPRequestMiddleware) HandleFinalize( - ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler, -) ( - out middleware.FinalizeOutput, metadata middleware.Metadata, err error, -) { - req, ok := in.Request.(*smithyHTTP.Request) - if !ok { - return out, metadata, &SigningError{ - Err: fmt.Errorf("unexpected request middleware type %T", in.Request), - } - } - - httpReq := req.Build(ctx) - if !haveCredentialProvider(s.credentialsProvider) { - out.Result = &PresignedHTTPRequest{ - URL: httpReq.URL.String(), - Method: httpReq.Method, - SignedHeader: http.Header{}, - } - - return out, metadata, nil - } - - signingName := awsmiddleware.GetSigningName(ctx) - signingRegion := awsmiddleware.GetSigningRegion(ctx) - payloadHash := GetPayloadHash(ctx) - if len(payloadHash) == 0 { - return out, metadata, &SigningError{ - Err: fmt.Errorf("computed payload hash missing from context"), - } - } - - credentials, err := s.credentialsProvider.Retrieve(ctx) - if err != nil { - return out, metadata, &SigningError{ - Err: fmt.Errorf("failed to retrieve credentials: %w", err), - } - } - - u, h, err := s.presigner.PresignHTTP(ctx, credentials, - httpReq, payloadHash, signingName, signingRegion, time.Now(), - func(o *SignerOptions) { - o.Logger = middleware.GetLogger(ctx) - o.LogSigning = s.logSigning - }) - if err != nil { - return out, metadata, &SigningError{ - Err: fmt.Errorf("failed to sign http request, %w", err), - } - } - - out.Result = &PresignedHTTPRequest{ - URL: u, - Method: httpReq.Method, - SignedHeader: h, - } - - return out, metadata, nil -} diff --git a/api/handler/delete_test.go b/api/handler/delete_test.go index 157f000..83299de 100644 --- a/api/handler/delete_test.go +++ b/api/handler/delete_test.go @@ -3,6 +3,7 @@ package handler import ( "bytes" "encoding/xml" + "io" "net/http" "net/http/httptest" "net/url" @@ -14,9 +15,6 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/encryption" apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/private/protocol/xml/xmlutil" - "github.com/aws/aws-sdk-go/service/s3" "github.com/stretchr/testify/require" ) @@ -139,16 +137,17 @@ func TestDeleteObjectsError(t *testing.T) { hc.tp.SetObjectError(addr, expectedError) w := deleteObjectsBase(hc, bktName, [][2]string{{objName, nodeVersion.OID.EncodeToString()}}) - - res := &s3.DeleteObjectsOutput{} - err = xmlutil.UnmarshalXML(res, xml.NewDecoder(w.Result().Body), "") + var buf bytes.Buffer + res := &DeleteObjectsResponse{} + err = xml.NewDecoder(io.TeeReader(w.Result().Body, &buf)).Decode(res) require.NoError(t, err) - require.ElementsMatch(t, []*s3.Error{{ - Code: aws.String(expectedError.Code), - Key: aws.String(objName), - Message: aws.String(expectedError.Error()), - VersionId: aws.String(nodeVersion.OID.EncodeToString()), + require.Contains(t, buf.String(), "VersionId") + require.ElementsMatch(t, []DeleteError{{ + Code: expectedError.Code, + Key: objName, + Message: expectedError.Error(), + VersionID: nodeVersion.OID.EncodeToString(), }}, res.Errors) } diff --git a/api/handler/put_test.go b/api/handler/put_test.go index f64123f..fa877c8 100644 --- a/api/handler/put_test.go +++ b/api/handler/put_test.go @@ -19,14 +19,14 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth" - v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4" + v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/v4" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" - "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/stretchr/testify/require" ) @@ -451,8 +451,8 @@ func getChunkedRequest(ctx context.Context, t *testing.T, bktName, objName strin AWSAccessKeyID := "AKIAIOSFODNN7EXAMPLE" AWSSecretAccessKey := "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" - awsCreds := credentials.NewStaticCredentials(AWSAccessKeyID, AWSSecretAccessKey, "") - signer := v4.NewSigner(awsCreds) + awsCreds := aws.Credentials{AccessKeyID: AWSAccessKeyID, SecretAccessKey: AWSSecretAccessKey} + signer := v4.NewSigner() reqBody := bytes.NewBufferString("10000;chunk-signature=ad80c730a21e5b8d04586a2213dd63b9a0e99e0e2307b0ade35a65485a288648\r\n") _, err := reqBody.Write(chunk1) @@ -475,7 +475,7 @@ func getChunkedRequest(ctx context.Context, t *testing.T, bktName, objName strin signTime, err := time.Parse("20060102T150405Z", "20130524T000000Z") require.NoError(t, err) - _, err = signer.Sign(req, nil, "s3", "us-east-1", signTime) + err = signer.SignHTTP(ctx, awsCreds, req, auth.UnsignedPayload, "s3", "us-east-1", signTime) require.NoError(t, err) req.Body = io.NopCloser(reqBody) diff --git a/api/handler/s3reader.go b/api/handler/s3reader.go index 6b0cfb7..c23e387 100644 --- a/api/handler/s3reader.go +++ b/api/handler/s3reader.go @@ -3,16 +3,17 @@ package handler import ( "bufio" "bytes" + "context" "encoding/hex" "errors" "io" "net/http" "time" - v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4" + v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/v4" errs "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" - "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go-v2/aws" ) const ( @@ -22,6 +23,7 @@ const ( type ( s3ChunkReader struct { + ctx context.Context reader *bufio.Reader streamSigner *v4.StreamSigner @@ -166,7 +168,7 @@ func (c *s3ChunkReader) Read(buf []byte) (num int, err error) { // Once we have read the entire chunk successfully, we verify // that the received signature matches our computed signature. - calculatedSignature, err := c.streamSigner.GetSignature(nil, c.buffer, c.requestTime) + calculatedSignature, err := c.streamSigner.GetSignature(c.ctx, nil, c.buffer, c.requestTime) if err != nil { c.err = err return num, c.err @@ -189,29 +191,31 @@ func (c *s3ChunkReader) Read(buf []byte) (num int, err error) { } func newSignV4ChunkedReader(req *http.Request) (io.ReadCloser, error) { - box, err := middleware.GetBoxData(req.Context()) + ctx := req.Context() + box, err := middleware.GetBoxData(ctx) if err != nil { return nil, errs.GetAPIError(errs.ErrAuthorizationHeaderMalformed) } - authHeaders, err := middleware.GetAuthHeaders(req.Context()) + authHeaders, err := middleware.GetAuthHeaders(ctx) if err != nil { return nil, errs.GetAPIError(errs.ErrAuthorizationHeaderMalformed) } - currentCredentials := credentials.NewStaticCredentials(authHeaders.AccessKeyID, box.Gate.SecretKey, "") + currentCredentials := aws.Credentials{AccessKeyID: authHeaders.AccessKeyID, SecretAccessKey: box.Gate.SecretKey} seed, err := hex.DecodeString(authHeaders.SignatureV4) if err != nil { return nil, errs.GetAPIError(errs.ErrSignatureDoesNotMatch) } - reqTime, err := middleware.GetClientTime(req.Context()) + reqTime, err := middleware.GetClientTime(ctx) if err != nil { return nil, errs.GetAPIError(errs.ErrMalformedDate) } - newStreamSigner := v4.NewStreamSigner(authHeaders.Region, "s3", seed, currentCredentials) + newStreamSigner := v4.NewStreamSigner(currentCredentials, "s3", authHeaders.Region, seed) return &s3ChunkReader{ + ctx: ctx, reader: bufio.NewReader(req.Body), streamSigner: newStreamSigner, requestTime: reqTime, diff --git a/cmd/s3-authmate/modules/generate-presigned-url.go b/cmd/s3-authmate/modules/generate-presigned-url.go index f29d631..e7b6163 100644 --- a/cmd/s3-authmate/modules/generate-presigned-url.go +++ b/cmd/s3-authmate/modules/generate-presigned-url.go @@ -9,10 +9,8 @@ import ( "time" "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/credentials" - "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -61,29 +59,41 @@ func initGeneratePresignedURLCmd() { _ = generatePresignedURLCmd.MarkFlagRequired(objectFlag) } -func runGeneratePresignedURLCmd(*cobra.Command, []string) error { - var cfg aws.Config +func runGeneratePresignedURLCmd(cmd *cobra.Command, _ []string) error { + var ( + region string + creds aws.Credentials + ) - if region := viper.GetString(regionFlag); region != "" { - cfg.Region = ®ion - } - accessKeyID := viper.GetString(awsAccessKeyIDFlag) - secretAccessKey := viper.GetString(awsSecretAccessKeyFlag) + profile := viper.GetString(profileFlag) - if accessKeyID != "" && secretAccessKey != "" { - cfg.Credentials = credentials.NewStaticCredentialsFromCreds(credentials.Value{ - AccessKeyID: accessKeyID, - SecretAccessKey: secretAccessKey, - }) + if profile == "" { + cfg, err := config.LoadDefaultConfig(cmd.Context()) + if err != nil { + return wrapPreparationError(err) + } + region = cfg.Region + if creds, err = cfg.Credentials.Retrieve(cmd.Context()); err != nil { + return wrapPreparationError(fmt.Errorf("couldn't get default aws credentials: %w", err)) + } + } else { + cfg, err := config.LoadSharedConfigProfile(cmd.Context(), viper.GetString(profileFlag)) + if err != nil { + return wrapPreparationError(fmt.Errorf("couldn't get '%s' aws credentials: %w", viper.GetString(profileFlag), err)) + } + region = cfg.Region + creds = cfg.Credentials } - 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)) + accessKeyIDArg := viper.GetString(awsAccessKeyIDFlag) + secretAccessKeyArg := viper.GetString(awsSecretAccessKeyFlag) + if accessKeyIDArg != "" && secretAccessKeyArg != "" { + creds.AccessKeyID = accessKeyIDArg + creds.SecretAccessKey = secretAccessKeyArg + } + + if regionArg := viper.GetString(regionFlag); regionArg != "" { + region = regionArg } reqData := auth.RequestData{ @@ -94,7 +104,7 @@ func runGeneratePresignedURLCmd(*cobra.Command, []string) error { } presignData := auth.PresignData{ Service: "s3", - Region: *sess.Config.Region, + Region: region, Lifetime: viper.GetDuration(lifetimeFlag), SignTime: time.Now().UTC(), } @@ -107,15 +117,9 @@ func runGeneratePresignedURLCmd(*cobra.Command, []string) error { 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) + req, err = auth.PresignRequestV4a(creds, reqData, presignData) } else { - req, err = auth.PresignRequest(sess.Config.Credentials, reqData, presignData) + req, err = auth.PresignRequest(cmd.Context(), creds, reqData, presignData) } if err != nil { return wrapBusinessLogicError(err) diff --git a/cmd/s3-authmate/modules/sign.go b/cmd/s3-authmate/modules/sign.go index 002df44..e2005e8 100644 --- a/cmd/s3-authmate/modules/sign.go +++ b/cmd/s3-authmate/modules/sign.go @@ -8,9 +8,8 @@ import ( "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/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -25,7 +24,8 @@ 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`, +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 +frostfs-s3-authmate sign --data some-data --sigv4a`, RunE: runSignCmd, } @@ -43,35 +43,44 @@ func initSignCmd() { 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.Flags().Bool(sigV4AFlag, false, "Use SigV4A for signing request") _ = signCmd.MarkFlagRequired(dataFlag) } func runSignCmd(cmd *cobra.Command, _ []string) error { - var cfg aws.Config + var ( + region string + creds aws.Credentials + ) - 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)) + if profile := viper.GetString(profileFlag); profile == "" { + cfg, err := config.LoadDefaultConfig(cmd.Context()) + if err != nil { + return wrapPreparationError(err) + } + region = cfg.Region + if creds, err = cfg.Credentials.Retrieve(cmd.Context()); err != nil { + return wrapPreparationError(fmt.Errorf("couldn't get default aws credentials: %w", err)) + } + } else { + cfg, err := config.LoadSharedConfigProfile(cmd.Context(), profile) + if err != nil { + return wrapPreparationError(fmt.Errorf("couldn't get '%s' aws credentials: %w", profile, err)) + } + region = cfg.Region + creds = cfg.Credentials } - 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)) + accessKeyIDArg := viper.GetString(awsAccessKeyIDFlag) + secretAccessKeyArg := viper.GetString(awsSecretAccessKeyFlag) + if accessKeyIDArg != "" && secretAccessKeyArg != "" { + creds.AccessKeyID = accessKeyIDArg + creds.SecretAccessKey = secretAccessKeyArg + } + + if regionArg := viper.GetString(regionFlag); regionArg != "" { + region = regionArg } data := viper.GetString(dataFlag) @@ -83,15 +92,6 @@ func runSignCmd(cmd *cobra.Command, _ []string) error { 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")) @@ -102,11 +102,23 @@ func runSignCmd(cmd *cobra.Command, _ []string) error { signTime = time.Now() } - signature := auth.SignStr(creds.SecretAccessKey, service, *sess.Config.Region, signTime, data) + var signature string + sigv4a := viper.GetBool(sigV4AFlag) + if sigv4a { + var err error + if signature, err = auth.SignStrV4A(creds, data); err != nil { + return wrapPreparationError(fmt.Errorf("sign v4a: %w", err)) + } + } else { + signature = auth.SignStr(creds.SecretAccessKey, service, region, signTime, data) + } + + if !sigv4a { + cmd.Println("service:", service) + cmd.Println("region:", region) + cmd.Println("time:", signTime.UTC().Format("20060102")) + } - 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) diff --git a/go.mod b/go.mod index daf8bb9..5d5f2ee 100644 --- a/go.mod +++ b/go.mod @@ -10,10 +10,10 @@ require ( git.frostfs.info/TrueCloudLab/multinet v0.0.0-20241015075604-6cb0d80e0972 git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240822104152-a3bc3099bd5b git.frostfs.info/TrueCloudLab/zapjournald v0.0.0-20240124114243-cb2e66427d02 - github.com/aws/aws-sdk-go v1.44.6 - github.com/aws/aws-sdk-go-v2 v1.30.3 - github.com/aws/aws-sdk-go-v2/credentials v1.17.27 - github.com/aws/smithy-go v1.20.1 + github.com/aws/aws-sdk-go-v2 v1.30.5 + github.com/aws/aws-sdk-go-v2/config v1.27.32 + github.com/aws/aws-sdk-go-v2/credentials v1.17.31 + github.com/aws/smithy-go v1.20.4 github.com/bluele/gcache v0.0.2 github.com/go-chi/chi/v5 v5.0.8 github.com/google/uuid v1.6.0 @@ -50,7 +50,15 @@ require ( git.frostfs.info/TrueCloudLab/tzhash v1.8.0 // indirect github.com/VictoriaMetrics/easyproto v0.1.4 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect - github.com/aws/smithy-go v1.20.3 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.22.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.30.6 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -67,7 +75,6 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/holiman/uint256 v1.2.4 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect diff --git a/go.sum b/go.sum index 5934a8b..152ec14 100644 --- a/go.sum +++ b/go.sum @@ -64,14 +64,32 @@ github.com/VictoriaMetrics/easyproto v0.1.4 h1:r8cNvo8o6sR4QShBXQd1bKw/VVLSQma/V github.com/VictoriaMetrics/easyproto v0.1.4/go.mod h1:QlGlzaJnDfFd8Lk6Ci/fuLxfTo3/GThPs2KH23mv710= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= -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-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= -github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= -github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI= -github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4= -github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= -github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g= +github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0= +github.com/aws/aws-sdk-go-v2/config v1.27.32 h1:jnAMVTJTpAQlePCUUlnXnllHEMGVWmvUJOiGjgtS9S0= +github.com/aws/aws-sdk-go-v2/config v1.27.32/go.mod h1:JibtzKJoXT0M/MhoYL6qfCk7nm/MppwukDFZtdgVRoY= +github.com/aws/aws-sdk-go-v2/credentials v1.17.31 h1:jtyfcOfgoqWA2hW/E8sFbwdfgwD3APnF9CLCKE8dTyw= +github.com/aws/aws-sdk-go-v2/credentials v1.17.31/go.mod h1:RSgY5lfCfw+FoyKWtOpLolPlfQVdDBQWTUniAaE+NKY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 h1:pfQ2sqNpMVK6xz2RbqLEL0GH87JOwSxPV2rzm8Zsb74= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13/go.mod h1:NG7RXPUlqfsCLLFfi0+IpKN4sCB9D9fw/qTaSB+xRoU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 h1:pI7Bzt0BJtYA0N/JEC6B8fJ4RBrEMi1LBrkMdFYNSnQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17/go.mod h1:Dh5zzJYMtxfIjYW+/evjQ8uj2OyR/ve2KROHGHlSFqE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 h1:Mqr/V5gvrhA2gvgnF42Zh5iMiQNcOYthFYwCyrnuWlc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17/go.mod h1:aLJpZlCmjE+V+KtN1q1uyZkfnUWpQGpbsn89XPKyzfU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 h1:rfprUlsdzgl7ZL2KlXiUAoJnI/VxfHCvDFr2QDFj6u4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19/go.mod h1:SCWkEdRq8/7EK60NcvvQ6NXKuTcchAD4ROAsC37VEZE= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.6 h1:o++HUDXlbrTl4PSal3YHtdErQxB8mDGAtkKNXBWPfIU= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.6/go.mod h1:eEygMHnTKH/3kNp9Jr1n3PdejuSNcgwLe1dWgQtO0VQ= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.6 h1:yCHcQCOwTfIsc8DoEhM3qXPxD+j8CbI6t1K3dNzsWV0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.6/go.mod h1:bCbAxKDqNvkHxRaIMnyVPXPo+OaPRwvmgzMxbz1VKSA= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.6 h1:TrQadF7GcqvQ63kgwEcjlrVc2Fa0wpgLT0xtc73uAd8= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.6/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o= +github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4= +github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= 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/bits-and-blooms/bitset v1.8.0 h1:FD+XqgOZDUxxZ8hzoBFuV9+cGWY9CslN6d5MS5JVb4c= @@ -200,10 +218,6 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -427,7 +441,6 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -491,13 +504,11 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -506,7 +517,6 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -677,7 +687,6 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -- 2.45.2 From 1844cdb0d2d4fbce2191aa02d09bf346de1f1eb4 Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Thu, 5 Sep 2024 15:11:00 +0300 Subject: [PATCH 5/8] [#339] Presign fix aws sdk Signed-off-by: Denis Kirillov --- api/auth/signer/v4sdk2/signer/v4/v4.go | 66 ++++++++++++++------------ 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/api/auth/signer/v4sdk2/signer/v4/v4.go b/api/auth/signer/v4sdk2/signer/v4/v4.go index f79ed5c..11a2b3a 100644 --- a/api/auth/signer/v4sdk2/signer/v4/v4.go +++ b/api/auth/signer/v4sdk2/signer/v4/v4.go @@ -1,48 +1,41 @@ -// Package v4 implements signing for AWS V4 signer +// Package v4 implements the AWS signature version 4 algorithm (commonly known +// as SigV4). // -// Provides request signing for request that need to be signed with -// AWS V4 Signatures. +// For more information about SigV4, see [Signing AWS API requests] in the IAM +// user guide. // -// # Standalone Signer +// While this implementation CAN work in an external context, it is developed +// primarily for SDK use and you may encounter fringe behaviors around header +// canonicalization. // -// Generally using the signer outside of the SDK should not require any additional +// # Pre-escaping a request URI // -// The signer does this by taking advantage of the URL.EscapedPath method. If your request URI requires +// AWS v4 signature validation requires that the canonical string's URI path +// component must be the escaped form of the HTTP request's path. // -// additional escaping you many need to use the URL.Opaque to define what the raw URI should be sent -// to the service as. +// The Go HTTP client will perform escaping automatically on the HTTP request. +// This may cause signature validation errors because the request differs from +// the URI path or query from which the signature was generated. // -// The signer will first check the URL.Opaque field, and use its value if set. -// The signer does require the URL.Opaque field to be set in the form of: +// Because of this, we recommend that you explicitly escape the request when +// using this signer outside of the SDK to prevent possible signature mismatch. +// This can be done by setting URL.Opaque on the request. The signer will +// prefer that value, falling back to the return of URL.EscapedPath if unset. +// +// When setting URL.Opaque you must do so in the form of: // // "///" // // // e.g. // "//example.com/some/path" // -// The leading "//" and hostname are required or the URL.Opaque escaping will -// not work correctly. +// The leading "//" and hostname are required or the escaping will not work +// correctly. // -// If URL.Opaque is not set the signer will fallback to the URL.EscapedPath() -// method and using the returned value. +// The TestStandaloneSign unit test provides a complete example of using the +// signer outside of the SDK and pre-escaping the URI path. // -// AWS v4 signature validation requires that the canonical string's URI path -// element must be the URI escaped form of the HTTP request's path. -// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html -// -// The Go HTTP client will perform escaping automatically on the request. Some -// of these escaping may cause signature validation errors because the HTTP -// request differs from the URI path or query that the signature was generated. -// https://golang.org/pkg/net/url/#URL.EscapedPath -// -// Because of this, it is recommended that when using the signer outside of the -// SDK that explicitly escaping the request prior to being signed is preferable, -// and will help prevent signature validation errors. This can be done by setting -// the URL.Opaque or URL.RawPath. The SDK will use URL.Opaque first and then -// call URL.EscapedPath() if Opaque is not set. -// -// Test `TestStandaloneSign` provides a complete example of using the signer -// outside of the SDK and pre-escaping the URI path. +// [Signing AWS API requests]: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-signing.html package v4 import ( @@ -411,7 +404,18 @@ func (s *httpSigner) buildCredentialScope() string { func buildQuery(r v4Internal.Rule, header http.Header) (url.Values, http.Header) { query := url.Values{} unsignedHeaders := http.Header{} + + // A list of headers to be converted to lower case to mitigate a limitation from S3 + lowerCaseHeaders := map[string]string{ + "X-Amz-Expected-Bucket-Owner": "x-amz-expected-bucket-owner", // see #2508 + "X-Amz-Request-Payer": "x-amz-request-payer", // see #2764 + } + for k, h := range header { + if newKey, ok := lowerCaseHeaders[k]; ok { + k = newKey + } + if r.IsValid(k) { query[k] = h } else { -- 2.45.2 From 58b193c8d94184610bf99da2f6955f93f1903bd0 Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Thu, 5 Sep 2024 15:34:54 +0300 Subject: [PATCH 6/8] [#339] lint: Ignore aws sdk dirs Signed-off-by: Denis Kirillov --- .golangci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index 7f969d4..8916aa6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -66,3 +66,6 @@ issues: - EXC0003 # test/Test ... consider calling this - EXC0004 # govet - EXC0005 # C-style breaks + exclude-dirs: + - api/auth/signer/v4asdk2 + - api/auth/signer/v4sdk2 -- 2.45.2 From 09ffe7287a8eb8f613c70db8f5e261406e8ee73c Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Wed, 27 Nov 2024 11:31:53 +0300 Subject: [PATCH 7/8] [#339] Fix logging in authmate [pre]sign command Signed-off-by: Denis Kirillov --- api/auth/center.go | 17 ++++------- api/auth/center_fuzz_test.go | 3 +- api/auth/presign.go | 30 ++++++++++++++----- api/auth/presign_test.go | 5 ++-- .../modules/generate-presigned-url.go | 16 ++++++---- cmd/s3-authmate/modules/sign.go | 14 +++++---- 6 files changed, 54 insertions(+), 31 deletions(-) diff --git a/api/auth/center.go b/api/auth/center.go index 1dbf239..adb998a 100644 --- a/api/auth/center.go +++ b/api/auth/center.go @@ -142,10 +142,6 @@ func (c *Center) parseAuthHeader(authHeader string, headers http.Header) (*AuthH default: return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrAuthorizationHeaderMalformed), authHeader) } - // AWS4-ECDSA-P256-SHA256 - // Credential=2XGRML5EW3LMHdf64W2DkBy1Nkuu4y4wGhUj44QjbXBi05ZNvs8WVwy1XTmSEkcVkydPKzCgtmR7U3zyLYTj3Snxf/20240326/s3/aws4_request, - // SignedHeaders=amz-sdk-invocation-id;amz-sdk-request;host;x-amz-content-sha256;x-amz-date;x-amz-region-set, - // Signature=3044022006a2bc760140834101d0a79667d6aa75768c1a28e9cafc8963484d0752a6c6050220629dc06d7d6505e1b1e2a5d1f974b25ba32fdffc6f3f70dc4dda31b8a6f7ea2b return &AuthHeader{ AccessKeyID: submatches["access_key_id"], @@ -487,20 +483,19 @@ func SignStr(secret, service, region string, t time.Time, strToSign string) stri return hex.EncodeToString(signature) } -func SignStrV4A(cred aws.Credentials, strToSign string) (string, error) { - hash := sha256.New() - hash.Write([]byte(strToSign)) - +func SignStrV4A(ctx context.Context, cred aws.Credentials, strToSign string) (string, error) { credAdapter := v4a.SymmetricCredentialAdaptor{ SymmetricProvider: credentials.NewStaticCredentialsProvider(cred.AccessKeyID, cred.SecretAccessKey, ""), } - creds, err := credAdapter.RetrievePrivateKey(context.Background()) // because of using StaticCredentialsProvider + creds, err := credAdapter.RetrievePrivateKey(ctx) if err != nil { - // no error is expected - panic(err) + return "", err } + hash := sha256.New() + hash.Write([]byte(strToSign)) + sig, err := creds.PrivateKey.Sign(rand.Reader, hash.Sum(nil), crypto.SHA256) if err != nil { return "", err diff --git a/api/auth/center_fuzz_test.go b/api/auth/center_fuzz_test.go index a5247da..9f5cfc8 100644 --- a/api/auth/center_fuzz_test.go +++ b/api/auth/center_fuzz_test.go @@ -13,6 +13,7 @@ import ( oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" "github.com/aws/aws-sdk-go-v2/aws" utils "github.com/trailofbits/go-fuzz-utils" + "go.uber.org/zap" ) const ( @@ -60,7 +61,7 @@ func DoFuzzAuthenticate(input []byte) int { SignTime: time.Now().UTC(), } - req, err := PresignRequest(context.Background(), awsCreds, reqData, presignData) + req, err := PresignRequest(context.Background(), awsCreds, reqData, presignData, zap.NewNop()) if req == nil { return fuzzFailExitCode } diff --git a/api/auth/presign.go b/api/auth/presign.go index 8d0f2d3..fa71255 100644 --- a/api/auth/presign.go +++ b/api/auth/presign.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "net/url" - "os" "strconv" "strings" "time" @@ -16,6 +15,7 @@ import ( "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/smithy-go/encoding/httpbinding" "github.com/aws/smithy-go/logging" + "go.uber.org/zap" ) type RequestData struct { @@ -34,7 +34,7 @@ type PresignData struct { } // PresignRequest forms pre-signed request to access objects without aws credentials. -func PresignRequest(ctx context.Context, creds aws.Credentials, reqData RequestData, presignData PresignData) (*http.Request, error) { +func PresignRequest(ctx context.Context, creds aws.Credentials, reqData RequestData, presignData PresignData, log *zap.Logger) (*http.Request, error) { urlStr := fmt.Sprintf("%s/%s/%s", reqData.Endpoint, httpbinding.EscapePath(reqData.Bucket, false), httpbinding.EscapePath(reqData.Object, false)) req, err := http.NewRequest(strings.ToUpper(reqData.Method), urlStr, nil) if err != nil { @@ -49,6 +49,8 @@ func PresignRequest(ctx context.Context, creds aws.Credentials, reqData RequestD signer := v4.NewSigner(func(options *v4.SignerOptions) { options.DisableURIPathEscaping = true + options.LogSigning = true + options.Logger = &logWrapper{log: log} }) signedURI, _, err := signer.PresignHTTP(ctx, creds, req, presignData.Headers[AmzContentSHA256], presignData.Service, presignData.Region, presignData.SignTime) @@ -64,7 +66,7 @@ func PresignRequest(ctx context.Context, creds aws.Credentials, reqData RequestD } // PresignRequestV4a forms pre-signed request to access objects without aws credentials. -func PresignRequestV4a(cred aws.Credentials, reqData RequestData, presignData PresignData) (*http.Request, error) { +func PresignRequestV4a(cred aws.Credentials, reqData RequestData, presignData PresignData, log *zap.Logger) (*http.Request, error) { urlStr := fmt.Sprintf("%s/%s/%s", reqData.Endpoint, httpbinding.EscapePath(reqData.Bucket, false), httpbinding.EscapePath(reqData.Object, false)) req, err := http.NewRequest(strings.ToUpper(reqData.Method), urlStr, nil) if err != nil { @@ -76,12 +78,12 @@ func PresignRequestV4a(cred aws.Credentials, reqData RequestData, presignData Pr } req.Header.Set(AmzDate, presignData.SignTime.Format("20060102T150405Z")) - req.Header.Set(AmzExpires, strconv.Itoa(int(presignData.Lifetime.Seconds()))) + req.Header.Set(AmzExpires, strconv.FormatFloat(presignData.Lifetime.Round(time.Second).Seconds(), 'f', 0, 64)) signer := v4a.NewSigner(func(options *v4a.SignerOptions) { options.DisableURIPathEscaping = true options.LogSigning = true - options.Logger = logging.NewStandardLogger(os.Stdout) + options.Logger = &logWrapper{log: log} }) credAdapter := v4a.SymmetricCredentialAdaptor{ @@ -97,7 +99,21 @@ func PresignRequestV4a(cred aws.Credentials, reqData RequestData, presignData Pr return nil, fmt.Errorf("presign: %w", err) } - fmt.Println(presignedURL) - return http.NewRequest(reqData.Method, presignedURL, nil) } + +type logWrapper struct { + log *zap.Logger +} + +func (l *logWrapper) Logf(classification logging.Classification, format string, args ...interface{}) { + msg := fmt.Sprintf(format, args...) + switch classification { + case logging.Warn: + l.log.Warn(msg) + case logging.Debug: + l.log.Debug(msg) + default: + l.log.Info(msg) + } +} diff --git a/api/auth/presign_test.go b/api/auth/presign_test.go index c91499b..bc2bf34 100644 --- a/api/auth/presign_test.go +++ b/api/auth/presign_test.go @@ -20,6 +20,7 @@ import ( credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/smithy-go/logging" "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" ) var _ tokens.Credentials = (*credentialsMock)(nil) @@ -83,7 +84,7 @@ func TestCheckSign(t *testing.T) { }, } - req, err := PresignRequest(ctx, awsCreds, reqData, presignData) + req, err := PresignRequest(ctx, awsCreds, reqData, presignData, zaptest.NewLogger(t)) require.NoError(t, err) expBox := &accessbox.Box{ @@ -131,7 +132,7 @@ func TestCheckSignV4a(t *testing.T) { }, } - req, err := PresignRequestV4a(awsCreds, reqData, presignData) + req, err := PresignRequestV4a(awsCreds, reqData, presignData, zaptest.NewLogger(t)) require.NoError(t, err) req.Header.Set(ContentTypeHdr, "text/plain") diff --git a/cmd/s3-authmate/modules/generate-presigned-url.go b/cmd/s3-authmate/modules/generate-presigned-url.go index e7b6163..330e766 100644 --- a/cmd/s3-authmate/modules/generate-presigned-url.go +++ b/cmd/s3-authmate/modules/generate-presigned-url.go @@ -1,6 +1,7 @@ package modules import ( + "context" "encoding/json" "fmt" "net/http" @@ -60,6 +61,11 @@ func initGeneratePresignedURLCmd() { } func runGeneratePresignedURLCmd(cmd *cobra.Command, _ []string) error { + ctx, cancel := context.WithTimeout(cmd.Context(), viper.GetDuration(timeoutFlag)) + defer cancel() + + log := getLogger() + var ( region string creds aws.Credentials @@ -68,16 +74,16 @@ func runGeneratePresignedURLCmd(cmd *cobra.Command, _ []string) error { profile := viper.GetString(profileFlag) if profile == "" { - cfg, err := config.LoadDefaultConfig(cmd.Context()) + cfg, err := config.LoadDefaultConfig(ctx) if err != nil { return wrapPreparationError(err) } region = cfg.Region - if creds, err = cfg.Credentials.Retrieve(cmd.Context()); err != nil { + if creds, err = cfg.Credentials.Retrieve(ctx); err != nil { return wrapPreparationError(fmt.Errorf("couldn't get default aws credentials: %w", err)) } } else { - cfg, err := config.LoadSharedConfigProfile(cmd.Context(), viper.GetString(profileFlag)) + cfg, err := config.LoadSharedConfigProfile(ctx, viper.GetString(profileFlag)) if err != nil { return wrapPreparationError(fmt.Errorf("couldn't get '%s' aws credentials: %w", viper.GetString(profileFlag), err)) } @@ -117,9 +123,9 @@ func runGeneratePresignedURLCmd(cmd *cobra.Command, _ []string) error { var req *http.Request if viper.GetBool(sigV4AFlag) { - req, err = auth.PresignRequestV4a(creds, reqData, presignData) + req, err = auth.PresignRequestV4a(creds, reqData, presignData, log) } else { - req, err = auth.PresignRequest(cmd.Context(), creds, reqData, presignData) + req, err = auth.PresignRequest(ctx, creds, reqData, presignData, log) } if err != nil { return wrapBusinessLogicError(err) diff --git a/cmd/s3-authmate/modules/sign.go b/cmd/s3-authmate/modules/sign.go index e2005e8..a20dde5 100644 --- a/cmd/s3-authmate/modules/sign.go +++ b/cmd/s3-authmate/modules/sign.go @@ -1,6 +1,7 @@ package modules import ( + "context" "errors" "fmt" "os" @@ -49,22 +50,25 @@ func initSignCmd() { } func runSignCmd(cmd *cobra.Command, _ []string) error { + ctx, cancel := context.WithTimeout(cmd.Context(), viper.GetDuration(timeoutFlag)) + defer cancel() + var ( region string creds aws.Credentials ) if profile := viper.GetString(profileFlag); profile == "" { - cfg, err := config.LoadDefaultConfig(cmd.Context()) + cfg, err := config.LoadDefaultConfig(ctx) if err != nil { return wrapPreparationError(err) } region = cfg.Region - if creds, err = cfg.Credentials.Retrieve(cmd.Context()); err != nil { + if creds, err = cfg.Credentials.Retrieve(ctx); err != nil { return wrapPreparationError(fmt.Errorf("couldn't get default aws credentials: %w", err)) } } else { - cfg, err := config.LoadSharedConfigProfile(cmd.Context(), profile) + cfg, err := config.LoadSharedConfigProfile(ctx, profile) if err != nil { return wrapPreparationError(fmt.Errorf("couldn't get '%s' aws credentials: %w", profile, err)) } @@ -106,8 +110,8 @@ func runSignCmd(cmd *cobra.Command, _ []string) error { sigv4a := viper.GetBool(sigV4AFlag) if sigv4a { var err error - if signature, err = auth.SignStrV4A(creds, data); err != nil { - return wrapPreparationError(fmt.Errorf("sign v4a: %w", err)) + if signature, err = auth.SignStrV4A(ctx, creds, data); err != nil { + return wrapPreparationError(fmt.Errorf("sign str v4a: %w", err)) } } else { signature = auth.SignStr(creds.SecretAccessKey, service, region, signTime, data) -- 2.45.2 From d7a1cfe6a318c4b9582007afb0255d4e26e0d077 Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Thu, 28 Nov 2024 12:37:06 +0300 Subject: [PATCH 8/8] [#339] Drop unused and add link to source files Signed-off-by: Denis Kirillov --- api/auth/signer/v4asdk2/credentials.go | 4 + api/auth/signer/v4asdk2/credentials_test.go | 2 + api/auth/signer/v4asdk2/error.go | 17 -- .../signer/v4asdk2/internal/crypto/compare.go | 2 + .../v4asdk2/internal/crypto/compare_test.go | 2 + .../signer/v4asdk2/internal/crypto/ecc.go | 2 + .../v4asdk2/internal/crypto/ecc_test.go | 2 + api/auth/signer/v4asdk2/internal/v4/const.go | 2 + .../v4asdk2/internal/v4/header_rules.go | 2 + .../signer/v4asdk2/internal/v4/headers.go | 4 + api/auth/signer/v4asdk2/internal/v4/hmac.go | 2 + api/auth/signer/v4asdk2/internal/v4/host.go | 2 + api/auth/signer/v4asdk2/internal/v4/time.go | 2 + api/auth/signer/v4asdk2/internal/v4/util.go | 2 + .../signer/v4asdk2/internal/v4/util_test.go | 2 + api/auth/signer/v4asdk2/middleware.go | 118 ---------- api/auth/signer/v4asdk2/middleware_test.go | 149 ------------ api/auth/signer/v4asdk2/presign_middleware.go | 116 --------- .../signer/v4asdk2/presign_middleware_test.go | 222 ------------------ api/auth/signer/v4asdk2/shared_test.go | 18 -- api/auth/signer/v4asdk2/smithy.go | 85 ------- api/auth/signer/v4asdk2/stream.go | 2 + api/auth/signer/v4asdk2/v4a.go | 6 + api/auth/signer/v4asdk2/v4a_test.go | 2 + .../signer/v4sdk2/signer/internal/v4/cache.go | 2 + .../signer/v4sdk2/signer/internal/v4/const.go | 2 + .../v4sdk2/signer/internal/v4/header_rules.go | 2 + .../v4sdk2/signer/internal/v4/headers.go | 4 + .../v4sdk2/signer/internal/v4/headers_test.go | 2 + .../signer/v4sdk2/signer/internal/v4/hmac.go | 2 + .../signer/v4sdk2/signer/internal/v4/host.go | 2 + .../signer/v4sdk2/signer/internal/v4/scope.go | 2 + .../signer/v4sdk2/signer/internal/v4/time.go | 2 + .../signer/v4sdk2/signer/internal/v4/util.go | 2 + .../v4sdk2/signer/internal/v4/util_test.go | 2 + api/auth/signer/v4sdk2/signer/v4/stream.go | 2 + api/auth/signer/v4sdk2/signer/v4/v4.go | 4 + api/auth/signer/v4sdk2/signer/v4/v4_test.go | 2 + 38 files changed, 74 insertions(+), 725 deletions(-) delete mode 100644 api/auth/signer/v4asdk2/error.go delete mode 100644 api/auth/signer/v4asdk2/middleware.go delete mode 100644 api/auth/signer/v4asdk2/middleware_test.go delete mode 100644 api/auth/signer/v4asdk2/presign_middleware.go delete mode 100644 api/auth/signer/v4asdk2/presign_middleware_test.go delete mode 100644 api/auth/signer/v4asdk2/shared_test.go delete mode 100644 api/auth/signer/v4asdk2/smithy.go diff --git a/api/auth/signer/v4asdk2/credentials.go b/api/auth/signer/v4asdk2/credentials.go index 152482d..04dbd38 100644 --- a/api/auth/signer/v4asdk2/credentials.go +++ b/api/auth/signer/v4asdk2/credentials.go @@ -1,3 +1,7 @@ +// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/credentials.go +// with changes: +// * use `time.Now()` instead of `sdk.NowTime()` + package v4a import ( diff --git a/api/auth/signer/v4asdk2/credentials_test.go b/api/auth/signer/v4asdk2/credentials_test.go index bc89ec3..2918eb2 100644 --- a/api/auth/signer/v4asdk2/credentials_test.go +++ b/api/auth/signer/v4asdk2/credentials_test.go @@ -1,3 +1,5 @@ +// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/credentials_test.go + package v4a import ( diff --git a/api/auth/signer/v4asdk2/error.go b/api/auth/signer/v4asdk2/error.go deleted file mode 100644 index 380d174..0000000 --- a/api/auth/signer/v4asdk2/error.go +++ /dev/null @@ -1,17 +0,0 @@ -package v4a - -import "fmt" - -// SigningError indicates an error condition occurred while performing SigV4a signing -type SigningError struct { - Err error -} - -func (e *SigningError) Error() string { - return fmt.Sprintf("failed to sign request: %v", e.Err) -} - -// Unwrap returns the underlying error cause -func (e *SigningError) Unwrap() error { - return e.Err -} diff --git a/api/auth/signer/v4asdk2/internal/crypto/compare.go b/api/auth/signer/v4asdk2/internal/crypto/compare.go index 1d0f25f..e928ed5 100644 --- a/api/auth/signer/v4asdk2/internal/crypto/compare.go +++ b/api/auth/signer/v4asdk2/internal/crypto/compare.go @@ -1,3 +1,5 @@ +// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/crypto/compare.go + package crypto import "fmt" diff --git a/api/auth/signer/v4asdk2/internal/crypto/compare_test.go b/api/auth/signer/v4asdk2/internal/crypto/compare_test.go index 2bbdfdb..f3d72d2 100644 --- a/api/auth/signer/v4asdk2/internal/crypto/compare_test.go +++ b/api/auth/signer/v4asdk2/internal/crypto/compare_test.go @@ -1,3 +1,5 @@ +// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/crypto/compare_test.go + package crypto import ( diff --git a/api/auth/signer/v4asdk2/internal/crypto/ecc.go b/api/auth/signer/v4asdk2/internal/crypto/ecc.go index 758c73f..5bd7ebf 100644 --- a/api/auth/signer/v4asdk2/internal/crypto/ecc.go +++ b/api/auth/signer/v4asdk2/internal/crypto/ecc.go @@ -1,3 +1,5 @@ +// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/crypto/ecc.go + package crypto import ( diff --git a/api/auth/signer/v4asdk2/internal/crypto/ecc_test.go b/api/auth/signer/v4asdk2/internal/crypto/ecc_test.go index 72a5e8d..77fe99d 100644 --- a/api/auth/signer/v4asdk2/internal/crypto/ecc_test.go +++ b/api/auth/signer/v4asdk2/internal/crypto/ecc_test.go @@ -1,3 +1,5 @@ +// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/crypto/ecc_test.go + package crypto import ( diff --git a/api/auth/signer/v4asdk2/internal/v4/const.go b/api/auth/signer/v4asdk2/internal/v4/const.go index 89a76e2..e6ea2b8 100644 --- a/api/auth/signer/v4asdk2/internal/v4/const.go +++ b/api/auth/signer/v4asdk2/internal/v4/const.go @@ -1,3 +1,5 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/const.go + package v4 const ( diff --git a/api/auth/signer/v4asdk2/internal/v4/header_rules.go b/api/auth/signer/v4asdk2/internal/v4/header_rules.go index 7a9d415..760efd8 100644 --- a/api/auth/signer/v4asdk2/internal/v4/header_rules.go +++ b/api/auth/signer/v4asdk2/internal/v4/header_rules.go @@ -1,3 +1,5 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/header_rules.go + package v4 import ( diff --git a/api/auth/signer/v4asdk2/internal/v4/headers.go b/api/auth/signer/v4asdk2/internal/v4/headers.go index f6fc7f6..f04baf3 100644 --- a/api/auth/signer/v4asdk2/internal/v4/headers.go +++ b/api/auth/signer/v4asdk2/internal/v4/headers.go @@ -1,3 +1,7 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/header.go +// with changes: +// * drop User-Agent header from ignored + package v4 // IgnoredPresignedHeaders is a list of headers that are ignored during signing diff --git a/api/auth/signer/v4asdk2/internal/v4/hmac.go b/api/auth/signer/v4asdk2/internal/v4/hmac.go index e7fa7a1..99bd331 100644 --- a/api/auth/signer/v4asdk2/internal/v4/hmac.go +++ b/api/auth/signer/v4asdk2/internal/v4/hmac.go @@ -1,3 +1,5 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/hmac.go + package v4 import ( diff --git a/api/auth/signer/v4asdk2/internal/v4/host.go b/api/auth/signer/v4asdk2/internal/v4/host.go index bf93659..7cf0a85 100644 --- a/api/auth/signer/v4asdk2/internal/v4/host.go +++ b/api/auth/signer/v4asdk2/internal/v4/host.go @@ -1,3 +1,5 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/host.go + package v4 import ( diff --git a/api/auth/signer/v4asdk2/internal/v4/time.go b/api/auth/signer/v4asdk2/internal/v4/time.go index 1de06a7..a4876fe 100644 --- a/api/auth/signer/v4asdk2/internal/v4/time.go +++ b/api/auth/signer/v4asdk2/internal/v4/time.go @@ -1,3 +1,5 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/time.go + package v4 import "time" diff --git a/api/auth/signer/v4asdk2/internal/v4/util.go b/api/auth/signer/v4asdk2/internal/v4/util.go index 741019b..57990cf 100644 --- a/api/auth/signer/v4asdk2/internal/v4/util.go +++ b/api/auth/signer/v4asdk2/internal/v4/util.go @@ -1,3 +1,5 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/util.go + package v4 import ( diff --git a/api/auth/signer/v4asdk2/internal/v4/util_test.go b/api/auth/signer/v4asdk2/internal/v4/util_test.go index c29c1fa..000a098 100644 --- a/api/auth/signer/v4asdk2/internal/v4/util_test.go +++ b/api/auth/signer/v4asdk2/internal/v4/util_test.go @@ -1,3 +1,5 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/tuil_test.go + package v4 import ( diff --git a/api/auth/signer/v4asdk2/middleware.go b/api/auth/signer/v4asdk2/middleware.go deleted file mode 100644 index e1b6612..0000000 --- a/api/auth/signer/v4asdk2/middleware.go +++ /dev/null @@ -1,118 +0,0 @@ -package v4a - -import ( - "context" - "fmt" - "net/http" - "time" - - awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" - v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" - "github.com/aws/smithy-go/middleware" - smithyhttp "github.com/aws/smithy-go/transport/http" -) - -// HTTPSigner is SigV4a HTTP signer implementation -type HTTPSigner interface { - SignHTTP(ctx context.Context, credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, optfns ...func(*SignerOptions)) error -} - -// SignHTTPRequestMiddlewareOptions is the middleware options for constructing a SignHTTPRequestMiddleware. -type SignHTTPRequestMiddlewareOptions struct { - Credentials CredentialsProvider - Signer HTTPSigner - LogSigning bool -} - -// SignHTTPRequestMiddleware is a middleware for signing an HTTP request using SigV4a. -type SignHTTPRequestMiddleware struct { - credentials CredentialsProvider - signer HTTPSigner - logSigning bool -} - -// NewSignHTTPRequestMiddleware constructs a SignHTTPRequestMiddleware using the given SignHTTPRequestMiddlewareOptions. -func NewSignHTTPRequestMiddleware(options SignHTTPRequestMiddlewareOptions) *SignHTTPRequestMiddleware { - return &SignHTTPRequestMiddleware{ - credentials: options.Credentials, - signer: options.Signer, - logSigning: options.LogSigning, - } -} - -// ID the middleware identifier. -func (s *SignHTTPRequestMiddleware) ID() string { - return "Signing" -} - -// HandleFinalize signs an HTTP request using SigV4a. -func (s *SignHTTPRequestMiddleware) HandleFinalize( - ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler, -) ( - out middleware.FinalizeOutput, metadata middleware.Metadata, err error, -) { - if !hasCredentialProvider(s.credentials) { - return next.HandleFinalize(ctx, in) - } - - req, ok := in.Request.(*smithyhttp.Request) - if !ok { - return out, metadata, fmt.Errorf("unexpected request middleware type %T", in.Request) - } - - signingName, signingRegion := awsmiddleware.GetSigningName(ctx), awsmiddleware.GetSigningRegion(ctx) - payloadHash := v4.GetPayloadHash(ctx) - if len(payloadHash) == 0 { - return out, metadata, &SigningError{Err: fmt.Errorf("computed payload hash missing from context")} - } - - credentials, err := s.credentials.RetrievePrivateKey(ctx) - if err != nil { - return out, metadata, &SigningError{Err: fmt.Errorf("failed to retrieve credentials: %w", err)} - } - - signerOptions := []func(o *SignerOptions){ - func(o *SignerOptions) { - o.Logger = middleware.GetLogger(ctx) - o.LogSigning = s.logSigning - }, - } - - // existing DisableURIPathEscaping is equivalent in purpose - // to authentication scheme property DisableDoubleEncoding - //disableDoubleEncoding, overridden := internalauth.GetDisableDoubleEncoding(ctx) // internalauth "github.com/aws/aws-sdk-go-v2/internal/auth" - //if overridden { - // signerOptions = append(signerOptions, func(o *SignerOptions) { - // o.DisableURIPathEscaping = disableDoubleEncoding - // }) - //} - - err = s.signer.SignHTTP(ctx, credentials, req.Request, payloadHash, signingName, []string{signingRegion}, time.Now().UTC(), signerOptions...) - if err != nil { - return out, metadata, &SigningError{Err: fmt.Errorf("failed to sign http request, %w", err)} - } - - return next.HandleFinalize(ctx, in) -} - -func hasCredentialProvider(p CredentialsProvider) bool { - if p == nil { - return false - } - - return true -} - -// RegisterSigningMiddleware registers the SigV4a signing middleware to the stack. If a signing middleware is already -// present, this provided middleware will be swapped. Otherwise the middleware will be added at the tail of the -// finalize step. -func RegisterSigningMiddleware(stack *middleware.Stack, signingMiddleware *SignHTTPRequestMiddleware) (err error) { - const signedID = "Signing" - _, present := stack.Finalize.Get(signedID) - if present { - _, err = stack.Finalize.Swap(signedID, signingMiddleware) - } else { - err = stack.Finalize.Add(signingMiddleware, middleware.After) - } - return err -} diff --git a/api/auth/signer/v4asdk2/middleware_test.go b/api/auth/signer/v4asdk2/middleware_test.go deleted file mode 100644 index 135a5c5..0000000 --- a/api/auth/signer/v4asdk2/middleware_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package v4a - -import ( - "bytes" - "context" - "errors" - "fmt" - "net/http" - "strings" - "testing" - "time" - - awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" - v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" - "github.com/aws/smithy-go/logging" - "github.com/aws/smithy-go/middleware" - smithyhttp "github.com/aws/smithy-go/transport/http" -) - -type stubCredentialsProviderFunc func(context.Context) (Credentials, error) - -func (f stubCredentialsProviderFunc) RetrievePrivateKey(ctx context.Context) (Credentials, error) { - return f(ctx) -} - -type httpSignerFunc func(ctx context.Context, credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, optFns ...func(*SignerOptions)) error - -func (f httpSignerFunc) SignHTTP(ctx context.Context, credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, optFns ...func(*SignerOptions)) error { - return f(ctx, credentials, r, payloadHash, service, regionSet, signingTime, optFns...) -} - -func TestSignHTTPRequestMiddleware(t *testing.T) { - cases := map[string]struct { - creds CredentialsProvider - hash string - logSigning bool - expectedErr interface{} - }{ - "success": { - creds: stubCredentials, - hash: "0123456789abcdef", - }, - "error": { - creds: stubCredentialsProviderFunc(func(ctx context.Context) (Credentials, error) { - return Credentials{}, fmt.Errorf("credential error") - }), - hash: "", - expectedErr: &SigningError{}, - }, - "nil creds": { - creds: nil, - }, - "with log signing": { - creds: stubCredentials, - hash: "0123456789abcdef", - logSigning: true, - }, - } - - const ( - signingName = "serviceId" - signingRegion = "regionName" - ) - - for name, tt := range cases { - t.Run(name, func(t *testing.T) { - c := &SignHTTPRequestMiddleware{ - credentials: tt.creds, - signer: httpSignerFunc( - func(ctx context.Context, - credentials Credentials, r *http.Request, payloadHash string, - service string, regionSet []string, signingTime time.Time, - optFns ...func(*SignerOptions), - ) error { - var options SignerOptions - for _, fn := range optFns { - fn(&options) - } - if options.Logger == nil { - t.Errorf("expect logger, got none") - } - if options.LogSigning { - options.Logger.Logf(logging.Debug, t.Name()) - } - - expectCreds, _ := tt.creds.RetrievePrivateKey(ctx) - if diff := cmpDiff(expectCreds, credentials); len(diff) > 0 { - t.Error(diff) - } - if e, a := tt.hash, payloadHash; e != a { - t.Errorf("expected %v, got %v", e, a) - } - if e, a := signingName, service; e != a { - t.Errorf("expected %v, got %v", e, a) - } - if diff := cmpDiff([]string{signingRegion}, regionSet); len(diff) > 0 { - t.Error(diff) - } - return nil - }), - logSigning: tt.logSigning, - } - - next := middleware.FinalizeHandlerFunc(func(ctx context.Context, in middleware.FinalizeInput) (out middleware.FinalizeOutput, metadata middleware.Metadata, err error) { - return out, metadata, err - }) - - ctx := awsmiddleware.SetSigningRegion( - awsmiddleware.SetSigningName(context.Background(), signingName), - signingRegion) - - var loggerBuf bytes.Buffer - logger := logging.NewStandardLogger(&loggerBuf) - ctx = middleware.SetLogger(ctx, logger) - - if len(tt.hash) != 0 { - ctx = v4.SetPayloadHash(ctx, tt.hash) - } - - _, _, err := c.HandleFinalize(ctx, middleware.FinalizeInput{ - Request: &smithyhttp.Request{Request: &http.Request{}}, - }, next) - if err != nil && tt.expectedErr == nil { - t.Errorf("expected no error, got %v", err) - } else if err != nil && tt.expectedErr != nil { - e, a := tt.expectedErr, err - if !errors.As(a, &e) { - t.Errorf("expected error type %T, got %T", e, a) - } - } else if err == nil && tt.expectedErr != nil { - t.Errorf("expected error, got nil") - } - - if tt.logSigning { - if e, a := t.Name(), loggerBuf.String(); !strings.Contains(a, e) { - t.Errorf("expect %v logged in %v", e, a) - } - } else { - if loggerBuf.Len() != 0 { - t.Errorf("expect no log, got %v", loggerBuf.String()) - } - } - }) - } -} - -var ( - _ middleware.FinalizeMiddleware = &SignHTTPRequestMiddleware{} -) diff --git a/api/auth/signer/v4asdk2/presign_middleware.go b/api/auth/signer/v4asdk2/presign_middleware.go deleted file mode 100644 index ecb0f9e..0000000 --- a/api/auth/signer/v4asdk2/presign_middleware.go +++ /dev/null @@ -1,116 +0,0 @@ -package v4a - -import ( - "context" - "fmt" - "net/http" - "time" - - awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" - v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" - "github.com/aws/smithy-go/middleware" - smithyHTTP "github.com/aws/smithy-go/transport/http" -) - -// HTTPPresigner is an interface to a SigV4a signer that can sign create a -// presigned URL for a HTTP requests. -type HTTPPresigner interface { - PresignHTTP( - ctx context.Context, credentials Credentials, r *http.Request, - payloadHash string, service string, regionSet []string, signingTime time.Time, - optFns ...func(*SignerOptions), - ) (url string, signedHeader http.Header, err error) -} - -// PresignHTTPRequestMiddlewareOptions is the options for the PresignHTTPRequestMiddleware middleware. -type PresignHTTPRequestMiddlewareOptions struct { - CredentialsProvider CredentialsProvider - Presigner HTTPPresigner - LogSigning bool -} - -// PresignHTTPRequestMiddleware provides the Finalize middleware for creating a -// presigned URL for an HTTP request. -// -// Will short circuit the middleware stack and not forward onto the next -// Finalize handler. -type PresignHTTPRequestMiddleware struct { - credentialsProvider CredentialsProvider - presigner HTTPPresigner - logSigning bool -} - -// NewPresignHTTPRequestMiddleware returns a new PresignHTTPRequestMiddleware -// initialized with the presigner. -func NewPresignHTTPRequestMiddleware(options PresignHTTPRequestMiddlewareOptions) *PresignHTTPRequestMiddleware { - return &PresignHTTPRequestMiddleware{ - credentialsProvider: options.CredentialsProvider, - presigner: options.Presigner, - logSigning: options.LogSigning, - } -} - -// ID provides the middleware ID. -func (*PresignHTTPRequestMiddleware) ID() string { return "PresignHTTPRequest" } - -// HandleFinalize will take the provided input and create a presigned url for -// the http request using the SigV4 presign authentication scheme. -func (s *PresignHTTPRequestMiddleware) HandleFinalize( - ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler, -) ( - out middleware.FinalizeOutput, metadata middleware.Metadata, err error, -) { - req, ok := in.Request.(*smithyHTTP.Request) - if !ok { - return out, metadata, &SigningError{ - Err: fmt.Errorf("unexpected request middleware type %T", in.Request), - } - } - - httpReq := req.Build(ctx) - if !hasCredentialProvider(s.credentialsProvider) { - out.Result = &v4.PresignedHTTPRequest{ - URL: httpReq.URL.String(), - Method: httpReq.Method, - SignedHeader: http.Header{}, - } - - return out, metadata, nil - } - - signingName := awsmiddleware.GetSigningName(ctx) - signingRegion := awsmiddleware.GetSigningRegion(ctx) - payloadHash := v4.GetPayloadHash(ctx) - if len(payloadHash) == 0 { - return out, metadata, &SigningError{ - Err: fmt.Errorf("computed payload hash missing from context"), - } - } - - credentials, err := s.credentialsProvider.RetrievePrivateKey(ctx) - if err != nil { - return out, metadata, &SigningError{ - Err: fmt.Errorf("failed to retrieve credentials: %w", err), - } - } - - u, h, err := s.presigner.PresignHTTP(ctx, credentials, - httpReq, payloadHash, signingName, []string{signingRegion}, time.Now(), - func(o *SignerOptions) { - o.Logger = middleware.GetLogger(ctx) - o.LogSigning = s.logSigning - }) - if err != nil { - return out, metadata, &SigningError{ - Err: fmt.Errorf("failed to sign http request, %w", err), - } - } - - out.Result = &v4.PresignedHTTPRequest{ - URL: u, - Method: httpReq.Method, - SignedHeader: h, - } - - return out, metadata, nil -} diff --git a/api/auth/signer/v4asdk2/presign_middleware_test.go b/api/auth/signer/v4asdk2/presign_middleware_test.go deleted file mode 100644 index ec83a81..0000000 --- a/api/auth/signer/v4asdk2/presign_middleware_test.go +++ /dev/null @@ -1,222 +0,0 @@ -package v4a - -import ( - "bytes" - "context" - "net/http" - "net/url" - "strings" - "testing" - "time" - - awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" - v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" - "github.com/aws/smithy-go/logging" - "github.com/aws/smithy-go/middleware" - smithyhttp "github.com/aws/smithy-go/transport/http" -) - -type httpPresignerFunc func( - ctx context.Context, credentials Credentials, r *http.Request, - payloadHash string, service string, regionSet []string, signingTime time.Time, - optFns ...func(*SignerOptions), -) (url string, signedHeader http.Header, err error) - -func (f httpPresignerFunc) PresignHTTP( - ctx context.Context, credentials Credentials, r *http.Request, - payloadHash string, service string, regionSet []string, signingTime time.Time, - optFns ...func(*SignerOptions), -) ( - url string, signedHeader http.Header, err error, -) { - return f(ctx, credentials, r, payloadHash, service, regionSet, signingTime, optFns...) -} - -func TestPresignHTTPRequestMiddleware(t *testing.T) { - cases := map[string]struct { - Request *http.Request - Creds CredentialsProvider - PayloadHash string - LogSigning bool - ExpectResult *v4.PresignedHTTPRequest - ExpectErr string - }{ - "success": { - Request: &http.Request{ - URL: func() *url.URL { - u, _ := url.Parse("https://example.aws/path?query=foo") - return u - }(), - Header: http.Header{}, - }, - Creds: stubCredentials, - PayloadHash: "0123456789abcdef", - ExpectResult: &v4.PresignedHTTPRequest{ - URL: "https://example.aws/path?query=foo", - SignedHeader: http.Header{}, - }, - }, - "error": { - Request: func() *http.Request { - return &http.Request{} - }(), - Creds: stubCredentials, - PayloadHash: "", - ExpectErr: "failed to sign request", - }, - "anonymous creds": { - Request: &http.Request{ - URL: func() *url.URL { - u, _ := url.Parse("https://example.aws/path?query=foo") - return u - }(), - Header: http.Header{}, - }, - Creds: stubCredentials, - PayloadHash: "", - ExpectErr: "failed to sign request", - ExpectResult: &v4.PresignedHTTPRequest{ - URL: "https://example.aws/path?query=foo", - SignedHeader: http.Header{}, - }, - }, - "nil creds": { - Request: &http.Request{ - URL: func() *url.URL { - u, _ := url.Parse("https://example.aws/path?query=foo") - return u - }(), - Header: http.Header{}, - }, - Creds: nil, - ExpectResult: &v4.PresignedHTTPRequest{ - URL: "https://example.aws/path?query=foo", - SignedHeader: http.Header{}, - }, - }, - "with log signing": { - Request: &http.Request{ - URL: func() *url.URL { - u, _ := url.Parse("https://example.aws/path?query=foo") - return u - }(), - Header: http.Header{}, - }, - Creds: stubCredentials, - PayloadHash: "0123456789abcdef", - ExpectResult: &v4.PresignedHTTPRequest{ - URL: "https://example.aws/path?query=foo", - SignedHeader: http.Header{}, - }, - - LogSigning: true, - }, - } - - const ( - signingName = "serviceId" - signingRegion = "regionName" - ) - - for name, tt := range cases { - t.Run(name, func(t *testing.T) { - m := &PresignHTTPRequestMiddleware{ - credentialsProvider: tt.Creds, - - presigner: httpPresignerFunc(func( - ctx context.Context, credentials Credentials, r *http.Request, - payloadHash string, service string, regionSet []string, signingTime time.Time, - optFns ...func(*SignerOptions), - ) (url string, signedHeader http.Header, err error) { - var options SignerOptions - for _, fn := range optFns { - fn(&options) - } - if options.Logger == nil { - t.Errorf("expect logger, got none") - } - if options.LogSigning { - options.Logger.Logf(logging.Debug, t.Name()) - } - - if !hasCredentialProvider(tt.Creds) { - t.Errorf("expect presigner not to be called for not credentials provider") - } - - expectCreds, _ := tt.Creds.RetrievePrivateKey(context.Background()) - if diff := cmpDiff(expectCreds, credentials); len(diff) > 0 { - t.Error(diff) - } - if e, a := tt.PayloadHash, payloadHash; e != a { - t.Errorf("expected %v, got %v", e, a) - } - if e, a := signingName, service; e != a { - t.Errorf("expected %v, got %v", e, a) - } - if diff := cmpDiff([]string{signingRegion}, regionSet); len(diff) > 0 { - t.Error(diff) - } - - return tt.ExpectResult.URL, tt.ExpectResult.SignedHeader, nil - }), - logSigning: tt.LogSigning, - } - - next := middleware.FinalizeHandlerFunc( - func(ctx context.Context, in middleware.FinalizeInput) ( - out middleware.FinalizeOutput, metadata middleware.Metadata, err error, - ) { - t.Errorf("expect next handler not to be called") - return out, metadata, err - }) - - ctx := awsmiddleware.SetSigningRegion( - awsmiddleware.SetSigningName(context.Background(), signingName), - signingRegion) - - var loggerBuf bytes.Buffer - logger := logging.NewStandardLogger(&loggerBuf) - ctx = middleware.SetLogger(ctx, logger) - - if len(tt.PayloadHash) != 0 { - ctx = v4.SetPayloadHash(ctx, tt.PayloadHash) - } - - result, _, err := m.HandleFinalize(ctx, middleware.FinalizeInput{ - Request: &smithyhttp.Request{ - Request: tt.Request, - }, - }, next) - if len(tt.ExpectErr) != 0 { - if err == nil { - t.Fatalf("expect error, got none") - } - if e, a := tt.ExpectErr, err.Error(); !strings.Contains(a, e) { - t.Fatalf("expect error to contain %v, got %v", e, a) - } - return - } - if err != nil { - t.Fatalf("expect no error, got %v", err) - } - - if diff := cmpDiff(tt.ExpectResult, result.Result); len(diff) != 0 { - t.Errorf("expect result match\n%v", diff) - } - - if tt.LogSigning { - if e, a := t.Name(), loggerBuf.String(); !strings.Contains(a, e) { - t.Errorf("expect %v logged in %v", e, a) - } - } else { - if loggerBuf.Len() != 0 { - t.Errorf("expect no log, got %v", loggerBuf.String()) - } - } - }) - } -} - -var ( - _ middleware.FinalizeMiddleware = &PresignHTTPRequestMiddleware{} -) diff --git a/api/auth/signer/v4asdk2/shared_test.go b/api/auth/signer/v4asdk2/shared_test.go deleted file mode 100644 index 6c94400..0000000 --- a/api/auth/signer/v4asdk2/shared_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package v4a - -import ( - "bytes" - "context" - "crypto/ecdsa" -) - -var stubCredentials = stubCredentialsProviderFunc(func(ctx context.Context) (Credentials, error) { - stubKey, err := ecdsa.GenerateKey(p256, bytes.NewReader(bytes.Repeat([]byte{1}, 40))) - if err != nil { - return Credentials{}, err - } - return Credentials{ - Context: "STUB", - PrivateKey: stubKey, - }, nil -}) diff --git a/api/auth/signer/v4asdk2/smithy.go b/api/auth/signer/v4asdk2/smithy.go deleted file mode 100644 index 4ee4ca4..0000000 --- a/api/auth/signer/v4asdk2/smithy.go +++ /dev/null @@ -1,85 +0,0 @@ -package v4a - -import ( - "context" - "fmt" - "time" - - v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" - "github.com/aws/smithy-go" - "github.com/aws/smithy-go/auth" - "github.com/aws/smithy-go/logging" - smithyhttp "github.com/aws/smithy-go/transport/http" -) - -// CredentialsAdapter adapts v4a.Credentials to smithy auth.Identity. -type CredentialsAdapter struct { - Credentials Credentials -} - -var _ auth.Identity = (*CredentialsAdapter)(nil) - -// Expiration returns the time of expiration for the credentials. -func (v *CredentialsAdapter) Expiration() time.Time { - return v.Credentials.Expires -} - -// CredentialsProviderAdapter adapts v4a.CredentialsProvider to -// auth.IdentityResolver. -type CredentialsProviderAdapter struct { - Provider CredentialsProvider -} - -var _ (auth.IdentityResolver) = (*CredentialsProviderAdapter)(nil) - -// GetIdentity retrieves v4a credentials using the underlying provider. -func (v *CredentialsProviderAdapter) GetIdentity(ctx context.Context, _ smithy.Properties) ( - auth.Identity, error, -) { - creds, err := v.Provider.RetrievePrivateKey(ctx) - if err != nil { - return nil, fmt.Errorf("get credentials: %w", err) - } - - return &CredentialsAdapter{Credentials: creds}, nil -} - -// SignerAdapter adapts v4a.HTTPSigner to smithy http.Signer. -type SignerAdapter struct { - Signer HTTPSigner - Logger logging.Logger - LogSigning bool -} - -var _ (smithyhttp.Signer) = (*SignerAdapter)(nil) - -// SignRequest signs the request with the provided identity. -func (v *SignerAdapter) SignRequest(ctx context.Context, r *smithyhttp.Request, identity auth.Identity, props smithy.Properties) error { - ca, ok := identity.(*CredentialsAdapter) - if !ok { - return fmt.Errorf("unexpected identity type: %T", identity) - } - - name, ok := smithyhttp.GetSigV4SigningName(&props) - if !ok { - return fmt.Errorf("sigv4a signing name is required") - } - - regions, ok := smithyhttp.GetSigV4ASigningRegions(&props) - if !ok { - return fmt.Errorf("sigv4a signing region is required") - } - - hash := v4.GetPayloadHash(ctx) - err := v.Signer.SignHTTP(ctx, ca.Credentials, r.Request, hash, name, regions, time.Now(), func(o *SignerOptions) { - o.DisableURIPathEscaping, _ = smithyhttp.GetDisableDoubleEncoding(&props) - - o.Logger = v.Logger - o.LogSigning = v.LogSigning - }) - if err != nil { - return fmt.Errorf("sign http: %w", err) - } - - return nil -} diff --git a/api/auth/signer/v4asdk2/stream.go b/api/auth/signer/v4asdk2/stream.go index b168c6c..93988f9 100644 --- a/api/auth/signer/v4asdk2/stream.go +++ b/api/auth/signer/v4asdk2/stream.go @@ -1,3 +1,5 @@ +// This file is adopting https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/v4/stream.go for sigv4a. + package v4a import ( diff --git a/api/auth/signer/v4asdk2/v4a.go b/api/auth/signer/v4asdk2/v4a.go index f257ab5..5eaf72d 100644 --- a/api/auth/signer/v4asdk2/v4a.go +++ b/api/auth/signer/v4asdk2/v4a.go @@ -1,3 +1,9 @@ +// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/v4a.go +// with changes: +// * adding exported VerifySignature methods +// * using different ignore headers for sing/presign requests +// * don't duplicate content-length as signed header + package v4a import ( diff --git a/api/auth/signer/v4asdk2/v4a_test.go b/api/auth/signer/v4asdk2/v4a_test.go index a67ca20..40c6883 100644 --- a/api/auth/signer/v4asdk2/v4a_test.go +++ b/api/auth/signer/v4asdk2/v4a_test.go @@ -1,3 +1,5 @@ +// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/v4a_test.go + package v4a import ( diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/cache.go b/api/auth/signer/v4sdk2/signer/internal/v4/cache.go index cbf22f1..b1cf047 100644 --- a/api/auth/signer/v4sdk2/signer/internal/v4/cache.go +++ b/api/auth/signer/v4sdk2/signer/internal/v4/cache.go @@ -1,3 +1,5 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/cache.go + package v4 import ( diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/const.go b/api/auth/signer/v4sdk2/signer/internal/v4/const.go index a23cb00..9652f67 100644 --- a/api/auth/signer/v4sdk2/signer/internal/v4/const.go +++ b/api/auth/signer/v4sdk2/signer/internal/v4/const.go @@ -1,3 +1,5 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/const.go + package v4 // Signature Version 4 (SigV4) Constants diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/header_rules.go b/api/auth/signer/v4sdk2/signer/internal/v4/header_rules.go index 76fa34b..9036792 100644 --- a/api/auth/signer/v4sdk2/signer/internal/v4/header_rules.go +++ b/api/auth/signer/v4sdk2/signer/internal/v4/header_rules.go @@ -1,3 +1,5 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/header_rules.go + package v4 import ( diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/headers.go b/api/auth/signer/v4sdk2/signer/internal/v4/headers.go index f2a7766..46e220c 100644 --- a/api/auth/signer/v4sdk2/signer/internal/v4/headers.go +++ b/api/auth/signer/v4sdk2/signer/internal/v4/headers.go @@ -1,3 +1,7 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/header.go +// with changes: +// * drop User-Agent header from ignored + package v4 // IgnoredPresignedHeaders is a list of headers that are ignored during signing diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/headers_test.go b/api/auth/signer/v4sdk2/signer/internal/v4/headers_test.go index 6405ea9..bb38659 100644 --- a/api/auth/signer/v4sdk2/signer/internal/v4/headers_test.go +++ b/api/auth/signer/v4sdk2/signer/internal/v4/headers_test.go @@ -1,3 +1,5 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/header_test.go + package v4 import "testing" diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/hmac.go b/api/auth/signer/v4sdk2/signer/internal/v4/hmac.go index e7fa7a1..04534d7 100644 --- a/api/auth/signer/v4sdk2/signer/internal/v4/hmac.go +++ b/api/auth/signer/v4sdk2/signer/internal/v4/hmac.go @@ -1,3 +1,5 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/hmac.go + package v4 import ( diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/host.go b/api/auth/signer/v4sdk2/signer/internal/v4/host.go index bf93659..b5fdbbd 100644 --- a/api/auth/signer/v4sdk2/signer/internal/v4/host.go +++ b/api/auth/signer/v4sdk2/signer/internal/v4/host.go @@ -1,3 +1,5 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/host.go + package v4 import ( diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/scope.go b/api/auth/signer/v4sdk2/signer/internal/v4/scope.go index fc78879..d913a15 100644 --- a/api/auth/signer/v4sdk2/signer/internal/v4/scope.go +++ b/api/auth/signer/v4sdk2/signer/internal/v4/scope.go @@ -1,3 +1,5 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/scope.go + package v4 import "strings" diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/time.go b/api/auth/signer/v4sdk2/signer/internal/v4/time.go index 1de06a7..6dd31cf 100644 --- a/api/auth/signer/v4sdk2/signer/internal/v4/time.go +++ b/api/auth/signer/v4sdk2/signer/internal/v4/time.go @@ -1,3 +1,5 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/time.go + package v4 import "time" diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/util.go b/api/auth/signer/v4sdk2/signer/internal/v4/util.go index d025dba..5340a92 100644 --- a/api/auth/signer/v4sdk2/signer/internal/v4/util.go +++ b/api/auth/signer/v4sdk2/signer/internal/v4/util.go @@ -1,3 +1,5 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/util.go + package v4 import ( diff --git a/api/auth/signer/v4sdk2/signer/internal/v4/util_test.go b/api/auth/signer/v4sdk2/signer/internal/v4/util_test.go index a38ef2d..9f873b6 100644 --- a/api/auth/signer/v4sdk2/signer/internal/v4/util_test.go +++ b/api/auth/signer/v4sdk2/signer/internal/v4/util_test.go @@ -1,3 +1,5 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/util_test.go + package v4 import ( diff --git a/api/auth/signer/v4sdk2/signer/v4/stream.go b/api/auth/signer/v4sdk2/signer/v4/stream.go index aa23387..9cf060f 100644 --- a/api/auth/signer/v4sdk2/signer/v4/stream.go +++ b/api/auth/signer/v4sdk2/signer/v4/stream.go @@ -1,3 +1,5 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/v4/stream.go + package v4 import ( diff --git a/api/auth/signer/v4sdk2/signer/v4/v4.go b/api/auth/signer/v4sdk2/signer/v4/v4.go index 11a2b3a..afcd464 100644 --- a/api/auth/signer/v4sdk2/signer/v4/v4.go +++ b/api/auth/signer/v4sdk2/signer/v4/v4.go @@ -1,3 +1,7 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/v4/v4.go +// with changes: +// * using different headers for sign/presign + // Package v4 implements the AWS signature version 4 algorithm (commonly known // as SigV4). // diff --git a/api/auth/signer/v4sdk2/signer/v4/v4_test.go b/api/auth/signer/v4sdk2/signer/v4/v4_test.go index 420afd9..fb7b7d1 100644 --- a/api/auth/signer/v4sdk2/signer/v4/v4_test.go +++ b/api/auth/signer/v4sdk2/signer/v4/v4_test.go @@ -1,3 +1,5 @@ +// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/v4/v4_test.go + package v4 import ( -- 2.45.2