forked from TrueCloudLab/frostfs-s3-gw
Compare commits
3 commits
feature/33
...
master
Author | SHA1 | Date | |
---|---|---|---|
3cd88d6204 | |||
e71ba5e22a | |||
e3141fc8e3 |
66 changed files with 1336 additions and 5042 deletions
|
@ -66,6 +66,3 @@ issues:
|
|||
- EXC0003 # test/Test ... consider calling this
|
||||
- EXC0004 # govet
|
||||
- EXC0005 # C-style breaks
|
||||
exclude-dirs:
|
||||
- api/auth/signer/v4asdk2
|
||||
- api/auth/signer/v4sdk2
|
||||
|
|
|
@ -4,6 +4,12 @@ This document outlines major changes between releases.
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.31.1] - 2024-11-28
|
||||
|
||||
### Fixed
|
||||
- Ignore precondition headers with invalid date format (#563)
|
||||
- MD5 calculation of object-part with SSE-C (#543)
|
||||
|
||||
## [0.31.0] - Rongbuk - 2024-11-20
|
||||
|
||||
### Fixed
|
||||
|
@ -342,4 +348,5 @@ To see CHANGELOG for older versions, refer to https://github.com/nspcc-dev/neofs
|
|||
[0.30.7]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.30.6...v0.30.7
|
||||
[0.30.8]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.30.7...v0.30.8
|
||||
[0.31.0]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.30.8...v0.31.0
|
||||
[Unreleased]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.31.0...master
|
||||
[0.31.1]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.31.0...v0.31.1
|
||||
[Unreleased]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.31.1...master
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
v0.31.0
|
||||
v0.31.1
|
||||
|
|
|
@ -2,47 +2,36 @@ package auth
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
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"
|
||||
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/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"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
)
|
||||
|
||||
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<access_key_id>[^/]+)/(?P<date>[^/]+)/(?P<region>[^/]*)/(?P<service>[^/]+)/aws4_request,\s*SignedHeaders=(?P<signed_header_fields>.+),\s*Signature=(?P<v4_signature>.+)`)
|
||||
// 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<access_key_id>[^/]+)/(?P<date>[^/]+)/(?P<region>[^/]*)/(?P<service>[^/]+)/aws4_request,\s*SignedHeaders=(?P<signed_header_fields>.+),\s*Signature=(?P<v4_signature>.+)`)
|
||||
|
||||
// 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<access_key_id>[^/]+)/(?P<date>[^/]+)/(?P<service>[^/]+)/aws4_request,\s*SignedHeaders=(?P<signed_header_fields>.+),\s*Signature=(?P<v4_signature>.+)`)
|
||||
|
||||
// postPolicyCredentialRegexp -- is regexp for credentials when uploading file using POST with policy.
|
||||
postPolicyCredentialRegexp = regexp.MustCompile(`(?P<access_key_id>[^/]+)/(?P<date>[^/]+)/(?P<region>[^/]*)/(?P<service>[^/]+)/aws4_request`)
|
||||
)
|
||||
// postPolicyCredentialRegexp -- is regexp for credentials when uploading file using POST with policy.
|
||||
var postPolicyCredentialRegexp = regexp.MustCompile(`(?P<access_key_id>[^/]+)/(?P<date>[^/]+)/(?P<region>[^/]*)/(?P<service>[^/]+)/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
|
||||
|
@ -58,26 +47,22 @@ type (
|
|||
AccessKeyID string
|
||||
Service string
|
||||
Region string
|
||||
Signature string
|
||||
SignatureV4 string
|
||||
SignedFields []string
|
||||
Date string
|
||||
IsPresigned bool
|
||||
Expiration time.Duration
|
||||
Preamble string
|
||||
PayloadHash string
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
authHeaderPartsNum = 6
|
||||
authHeaderV4aPartsNum = 5
|
||||
maxFormSizeMemory = 50 * 1048576 // 50 MB
|
||||
authHeaderPartsNum = 6
|
||||
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"
|
||||
|
@ -106,52 +91,27 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
signaturePreambleSigV4 = "AWS4-HMAC-SHA256"
|
||||
signaturePreambleSigV4A = "AWS4-ECDSA-P256-SHA256"
|
||||
)
|
||||
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
signedFields := strings.Split(submatches["signed_header_fields"], ";")
|
||||
|
||||
return &AuthHeader{
|
||||
AccessKeyID: submatches["access_key_id"],
|
||||
Service: submatches["service"],
|
||||
Region: region,
|
||||
Signature: submatches["v4_signature"],
|
||||
SignedFields: strings.Split(submatches["signed_header_fields"], ";"),
|
||||
Region: submatches["region"],
|
||||
SignatureV4: submatches["v4_signature"],
|
||||
SignedFields: signedFields,
|
||||
Date: submatches["date"],
|
||||
Preamble: preamble,
|
||||
PayloadHash: headers.Get(AmzContentSHA256),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -169,7 +129,7 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) {
|
|||
)
|
||||
|
||||
queryValues := r.URL.Query()
|
||||
if queryValues.Get(AmzAlgorithm) == signaturePreambleSigV4 {
|
||||
if queryValues.Get(AmzAlgorithm) == "AWS4-HMAC-SHA256" {
|
||||
creds := strings.Split(queryValues.Get(AmzCredential), "/")
|
||||
if len(creds) != 5 || creds[4] != "aws4_request" {
|
||||
return nil, fmt.Errorf("bad X-Amz-Credential")
|
||||
|
@ -178,37 +138,14 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) {
|
|||
AccessKeyID: creds[0],
|
||||
Service: creds[3],
|
||||
Region: creds[2],
|
||||
Signature: queryValues.Get(AmzSignature),
|
||||
SignatureV4: queryValues.Get(AmzSignature),
|
||||
SignedFields: strings.Split(queryValues.Get(AmzSignedHeaders), ";"),
|
||||
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("%w: couldn't parse X-Amz-Expires %v", apierr.GetAPIError(apierr.ErrMalformedExpires), 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,
|
||||
PayloadHash: r.Header.Get(AmzContentSHA256),
|
||||
}
|
||||
authHdr.Expiration, err = time.ParseDuration(queryValues.Get(AmzExpires) + "s")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: couldn't parse X-Amz-Expires %v", apierr.GetAPIError(apierr.ErrMalformedExpires), err)
|
||||
return nil, fmt.Errorf("couldn't parse X-Amz-Expires: %w", err)
|
||||
}
|
||||
signatureDateTimeStr = queryValues.Get(AmzDate)
|
||||
} else {
|
||||
|
@ -219,7 +156,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], r.Header)
|
||||
authHdr, err = c.parseAuthHeader(authHeaderField[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -251,7 +188,7 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) {
|
|||
}
|
||||
|
||||
clonedRequest := cloneRequest(r, authHdr)
|
||||
if err = c.checkSign(r.Context(), authHdr, box, clonedRequest, signatureDateTime); err != nil {
|
||||
if err = c.checkSign(authHdr, box, clonedRequest, signatureDateTime); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -260,7 +197,7 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) {
|
|||
AuthHeaders: &middleware.AuthHeader{
|
||||
AccessKeyID: authHdr.AccessKeyID,
|
||||
Region: authHdr.Region,
|
||||
SignatureV4: authHdr.Signature,
|
||||
SignatureV4: authHdr.SignatureV4,
|
||||
},
|
||||
Attributes: attrs,
|
||||
}
|
||||
|
@ -393,87 +330,38 @@ func cloneRequest(r *http.Request, authHeader *AuthHeader) *http.Request {
|
|||
return otherRequest
|
||||
}
|
||||
|
||||
func (c *Center) checkSign(ctx context.Context, authHeader *AuthHeader, box *accessbox.Box, request *http.Request, signatureDateTime time.Time) error {
|
||||
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
|
||||
|
||||
switch authHeader.Preamble {
|
||||
case signaturePreambleSigV4:
|
||||
creds := aws.Credentials{
|
||||
AccessKeyID: authHeader.AccessKeyID,
|
||||
SecretAccessKey: box.Gate.SecretKey,
|
||||
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))
|
||||
}
|
||||
signer := v4.NewSigner(func(options *v4.SignerOptions) {
|
||||
options.DisableURIPathEscaping = true
|
||||
})
|
||||
|
||||
if authHeader.IsPresigned {
|
||||
if err := checkPresignedDate(authHeader, signatureDateTime); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
u, err := url.ParseRequestURI(signedURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
signature = u.Query().Get(AmzSignature)
|
||||
} else {
|
||||
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"]
|
||||
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
|
||||
})
|
||||
|
||||
credAdapter := v4a.SymmetricCredentialAdaptor{
|
||||
SymmetricProvider: credentials.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)
|
||||
}
|
||||
|
||||
if !authHeader.IsPresigned {
|
||||
return signer.VerifySignature(creds, request, authHeader.PayloadHash, authHeader.Service,
|
||||
strings.Split(authHeader.Region, ","), signatureDateTime, authHeader.Signature)
|
||||
}
|
||||
|
||||
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)
|
||||
signature = c.reg.GetSubmatches(request.Header.Get(AuthorizationHdr))["v4_signature"]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
if authHeader.SignatureV4 != signature {
|
||||
return fmt.Errorf("%w: %s != %s: headers %v", apierr.GetAPIError(apierr.ErrSignatureDoesNotMatch),
|
||||
authHeader.SignatureV4, signature, authHeader.SignedFields)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -483,27 +371,6 @@ func SignStr(secret, service, region string, t time.Time, strToSign string) stri
|
|||
return hex.EncodeToString(signature)
|
||||
}
|
||||
|
||||
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(ctx)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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))
|
||||
|
|
|
@ -4,16 +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-v2/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
utils "github.com/trailofbits/go-fuzz-utils"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -43,10 +41,7 @@ func DoFuzzAuthenticate(input []byte) int {
|
|||
|
||||
accessKeyID := strings.ReplaceAll(accessKeyAddr.String(), "/", "0")
|
||||
secretKey, err := tp.GetString()
|
||||
if err != nil {
|
||||
return fuzzFailExitCode
|
||||
}
|
||||
awsCreds := aws.Credentials{AccessKeyID: accessKeyID, SecretAccessKey: secretKey}
|
||||
awsCreds := credentials.NewStaticCredentials(accessKeyID, secretKey, "")
|
||||
|
||||
reqData := RequestData{
|
||||
Method: "GET",
|
||||
|
@ -61,7 +56,7 @@ func DoFuzzAuthenticate(input []byte) int {
|
|||
SignTime: time.Now().UTC(),
|
||||
}
|
||||
|
||||
req, err := PresignRequest(context.Background(), awsCreds, reqData, presignData, zap.NewNop())
|
||||
req, err := PresignRequest(awsCreds, reqData, presignData)
|
||||
if req == nil {
|
||||
return fuzzFailExitCode
|
||||
}
|
||||
|
|
|
@ -8,13 +8,11 @@ import (
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
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"
|
||||
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/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,9 +23,7 @@ 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"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/smithy-go/logging"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
@ -64,10 +60,9 @@ func TestAuthHeaderParse(t *testing.T) {
|
|||
AccessKeyID: "oid0cid",
|
||||
Service: "s3",
|
||||
Region: "us-east-1",
|
||||
Signature: "2811ccb9e242f41426738fb1f",
|
||||
SignatureV4: "2811ccb9e242f41426738fb1f",
|
||||
SignedFields: []string{"host", "x-amz-content-sha256", "x-amz-date"},
|
||||
Date: "20210809",
|
||||
Preamble: signaturePreambleSigV4,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -76,7 +71,7 @@ func TestAuthHeaderParse(t *testing.T) {
|
|||
expected: nil,
|
||||
},
|
||||
} {
|
||||
authHeader, err := center.parseAuthHeader(tc.header, nil)
|
||||
authHeader, err := center.parseAuthHeader(tc.header)
|
||||
require.ErrorIs(t, err, tc.err, tc.header)
|
||||
require.Equal(t, tc.expected, authHeader, tc.header)
|
||||
}
|
||||
|
@ -95,52 +90,6 @@ func TestSignature(t *testing.T) {
|
|||
require.Equal(t, "dfbe886241d9e369cf4b329ca0f15eb27306c97aa1022cc0bb5a914c4ef87634", signature)
|
||||
}
|
||||
|
||||
func TestSignatureV4A(t *testing.T) {
|
||||
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: credentials.NewStaticCredentialsProvider(accessKeyID, secretKey, ""),
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
err = signer.VerifySignature(creds, req, "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD", service, regionSet, signingTime, signature)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCheckFormatContentSHA256(t *testing.T) {
|
||||
defaultErr := errors.GetAPIError(errors.ErrContentSHA256Mismatch)
|
||||
|
||||
|
@ -216,7 +165,6 @@ 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)
|
||||
|
||||
|
@ -247,8 +195,8 @@ func TestAuthenticate(t *testing.T) {
|
|||
frostfs := newFrostFSMock()
|
||||
frostfs.objects[accessKeyID] = &obj
|
||||
|
||||
awsCreds := aws.Credentials{AccessKeyID: accessKeyID, SecretAccessKey: secret.SecretKey}
|
||||
defaultSigner := v4.NewSigner()
|
||||
awsCreds := credentials.NewStaticCredentials(accessKeyID, secret.SecretKey, "")
|
||||
defaultSigner := v4.NewSigner(awsCreds)
|
||||
|
||||
service, region := "s3", "default"
|
||||
invalidValue := "invalid-value"
|
||||
|
@ -271,7 +219,7 @@ func TestAuthenticate(t *testing.T) {
|
|||
prefixes: []string{addr.Container().String()},
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
err = defaultSigner.SignHTTP(ctx, awsCreds, r, "", service, region, time.Now())
|
||||
_, err = defaultSigner.Sign(r, nil, service, region, time.Now())
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
|
@ -297,8 +245,8 @@ func TestAuthenticate(t *testing.T) {
|
|||
name: "invalid access key id format",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
cred := aws.Credentials{AccessKeyID: addr.Object().String(), SecretAccessKey: secret.SecretKey}
|
||||
err = v4.NewSigner().SignHTTP(ctx, cred, r, "", service, region, time.Now())
|
||||
signer := v4.NewSigner(credentials.NewStaticCredentials(addr.Object().String(), secret.SecretKey, ""))
|
||||
_, err = signer.Sign(r, nil, service, region, time.Now())
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
|
@ -310,7 +258,7 @@ func TestAuthenticate(t *testing.T) {
|
|||
prefixes: []string{addr.Object().String()},
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
err = defaultSigner.SignHTTP(ctx, awsCreds, r, "", service, region, time.Now())
|
||||
_, err = defaultSigner.Sign(r, nil, service, region, time.Now())
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
|
@ -321,8 +269,8 @@ func TestAuthenticate(t *testing.T) {
|
|||
name: "invalid access key id value",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
cred := aws.Credentials{AccessKeyID: accessKeyID[:len(accessKeyID)-4], SecretAccessKey: secret.SecretKey}
|
||||
err = v4.NewSigner().SignHTTP(ctx, cred, r, "", service, region, time.Now())
|
||||
signer := v4.NewSigner(credentials.NewStaticCredentials(accessKeyID[:len(accessKeyID)-4], secret.SecretKey, ""))
|
||||
_, err = signer.Sign(r, nil, service, region, time.Now())
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
|
@ -333,8 +281,8 @@ func TestAuthenticate(t *testing.T) {
|
|||
name: "unknown access key id",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
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())
|
||||
signer := v4.NewSigner(credentials.NewStaticCredentials(addr.Object().String()+"0"+addr.Container().String(), secret.SecretKey, ""))
|
||||
_, err = signer.Sign(r, nil, service, region, time.Now())
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
|
@ -344,8 +292,8 @@ func TestAuthenticate(t *testing.T) {
|
|||
name: "invalid signature",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
cred := aws.Credentials{AccessKeyID: accessKeyID, SecretAccessKey: "secret"}
|
||||
err = v4.NewSigner().SignHTTP(ctx, cred, r, "", service, region, time.Now())
|
||||
signer := v4.NewSigner(credentials.NewStaticCredentials(accessKeyID, "secret", ""))
|
||||
_, err = signer.Sign(r, nil, service, region, time.Now())
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
|
@ -357,7 +305,7 @@ func TestAuthenticate(t *testing.T) {
|
|||
prefixes: []string{addr.Container().String()},
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
err = defaultSigner.SignHTTP(ctx, awsCreds, r, "", service, region, time.Now())
|
||||
_, err = defaultSigner.Sign(r, nil, service, region, time.Now())
|
||||
r.Header.Set(AmzDate, invalidValue)
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
|
@ -369,7 +317,7 @@ func TestAuthenticate(t *testing.T) {
|
|||
prefixes: []string{addr.Container().String()},
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
err = defaultSigner.SignHTTP(ctx, awsCreds, r, "", service, region, time.Now())
|
||||
_, err = defaultSigner.Sign(r, nil, service, region, time.Now())
|
||||
r.Header.Set(AmzContentSHA256, invalidValue)
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
|
@ -380,10 +328,7 @@ func TestAuthenticate(t *testing.T) {
|
|||
name: "valid presign",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
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)
|
||||
_, err = defaultSigner.Presign(r, nil, service, region, time.Minute, time.Now())
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
|
@ -405,24 +350,20 @@ func TestAuthenticate(t *testing.T) {
|
|||
name: "presign, bad X-Amz-Expires",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
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)
|
||||
_, 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()
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrMalformedExpires,
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "presign, expired",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
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)
|
||||
_, err = defaultSigner.Presign(r, nil, service, region, time.Minute, time.Now().Add(-time.Minute))
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
|
@ -433,10 +374,7 @@ func TestAuthenticate(t *testing.T) {
|
|||
name: "presign, signature from future",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
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)
|
||||
_, err = defaultSigner.Presign(r, nil, service, region, time.Minute, time.Now().Add(time.Minute))
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
|
|
|
@ -1,21 +1,14 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
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"
|
||||
"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"
|
||||
"go.uber.org/zap"
|
||||
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/private/protocol/rest"
|
||||
)
|
||||
|
||||
type RequestData struct {
|
||||
|
@ -34,86 +27,25 @@ 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, log *zap.Logger) (*http.Request, error) {
|
||||
urlStr := fmt.Sprintf("%s/%s/%s", reqData.Endpoint, httpbinding.EscapePath(reqData.Bucket, false), httpbinding.EscapePath(reqData.Object, false))
|
||||
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))
|
||||
req, err := http.NewRequest(strings.ToUpper(reqData.Method), urlStr, nil)
|
||||
if err != nil {
|
||||
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(AmzExpires, strconv.FormatFloat(presignData.Lifetime.Round(time.Second).Seconds(), 'f', 0, 64))
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("presign: %w", err)
|
||||
for k, v := range presignData.Headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
if req.URL, err = url.ParseRequestURI(signedURI); err != nil {
|
||||
return nil, fmt.Errorf("parse signed URI: %w", err)
|
||||
signer := v4.NewSigner(creds)
|
||||
signer.DisableURIPathEscaping = true
|
||||
|
||||
if _, err = signer.Presign(req, nil, presignData.Service, presignData.Region, presignData.Lifetime, presignData.SignTime); err != nil {
|
||||
return nil, fmt.Errorf("presign: %w", err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// PresignRequestV4a forms pre-signed request to access objects without aws credentials.
|
||||
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 {
|
||||
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(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 = &logWrapper{log: log}
|
||||
})
|
||||
|
||||
credAdapter := v4a.SymmetricCredentialAdaptor{
|
||||
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.Headers[AmzContentSHA256], presignData.Service, []string{presignData.Region}, presignData.SignTime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("presign: %w", err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,25 +2,18 @@ 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"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/smithy-go/logging"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
var _ tokens.Credentials = (*credentialsMock)(nil)
|
||||
|
@ -57,15 +50,13 @@ 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 := aws.Credentials{AccessKeyID: accessKeyID, SecretAccessKey: secretKey}
|
||||
awsCreds := credentials.NewStaticCredentials(accessKeyID, secretKey, "")
|
||||
|
||||
reqData := RequestData{
|
||||
Method: "GET",
|
||||
|
@ -78,13 +69,9 @@ func TestCheckSign(t *testing.T) {
|
|||
Region: "spb",
|
||||
Lifetime: 10 * time.Minute,
|
||||
SignTime: time.Now().UTC(),
|
||||
Headers: map[string]string{
|
||||
ContentTypeHdr: "text/plain",
|
||||
AmzContentSHA256: UnsignedPayload,
|
||||
},
|
||||
}
|
||||
|
||||
req, err := PresignRequest(ctx, awsCreds, reqData, presignData, zaptest.NewLogger(t))
|
||||
req, err := PresignRequest(awsCreds, reqData, presignData)
|
||||
require.NoError(t, err)
|
||||
|
||||
expBox := &accessbox.Box{
|
||||
|
@ -106,99 +93,3 @@ 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 := aws.Credentials{AccessKeyID: accessKeyID, SecretAccessKey: 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(),
|
||||
Headers: map[string]string{
|
||||
ContentTypeHdr: "text/plain",
|
||||
},
|
||||
}
|
||||
|
||||
req, err := PresignRequestV4a(awsCreds, reqData, presignData, zaptest.NewLogger(t))
|
||||
require.NoError(t, err)
|
||||
|
||||
req.Header.Set(ContentTypeHdr, "text/plain")
|
||||
|
||||
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(AmzExpires, "600")
|
||||
|
||||
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)
|
||||
query := r.URL.Query()
|
||||
query.Del(AmzSignature)
|
||||
r.URL.RawQuery = query.Encode()
|
||||
|
||||
err = signer.VerifyPresigned(creds, r, "", service, regionSet, signingTime, signature)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
|
87
api/auth/signer/v4/header_rules.go
Normal file
87
api/auth/signer/v4/header_rules.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
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
|
||||
}
|
7
api/auth/signer/v4/options.go
Normal file
7
api/auth/signer/v4/options.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package v4
|
||||
|
||||
// WithUnsignedPayload will enable and set the UnsignedPayload field to
|
||||
// true of the signer.
|
||||
func WithUnsignedPayload(v4 *Signer) {
|
||||
v4.UnsignedPayload = true
|
||||
}
|
14
api/auth/signer/v4/request_context_go1.7.go
Normal file
14
api/auth/signer/v4/request_context_go1.7.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
//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()
|
||||
}
|
63
api/auth/signer/v4/stream.go
Normal file
63
api/auth/signer/v4/stream.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
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")
|
||||
}
|
25
api/auth/signer/v4/uri_path.go
Normal file
25
api/auth/signer/v4/uri_path.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
//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
|
||||
}
|
858
api/auth/signer/v4/v4.go
Normal file
858
api/auth/signer/v4/v4.go
Normal file
|
@ -0,0 +1,858 @@
|
|||
// 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:
|
||||
//
|
||||
// "//<hostname>/<path>"
|
||||
//
|
||||
// // 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)
|
||||
}
|
|
@ -1,144 +0,0 @@
|
|||
// 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 (
|
||||
"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)
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/credentials_test.go
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/crypto/compare.go
|
||||
|
||||
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
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/crypto/compare_test.go
|
||||
|
||||
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
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/crypto/ecc.go
|
||||
|
||||
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
|
||||
}
|
|
@ -1,279 +0,0 @@
|
|||
// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/crypto/ecc_test.go
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/const.go
|
||||
|
||||
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"
|
||||
)
|
|
@ -1,90 +0,0 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/header_rules.go
|
||||
|
||||
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)
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
// 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
|
||||
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-"},
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/hmac.go
|
||||
|
||||
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)
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/host.go
|
||||
|
||||
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
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/time.go
|
||||
|
||||
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
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/util.go
|
||||
|
||||
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
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/tuil_test.go
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
// This file is adopting https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/v4/stream.go for sigv4a.
|
||||
|
||||
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",
|
||||
}, "/")
|
||||
|
||||
}
|
|
@ -1,587 +0,0 @@
|
|||
// 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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"sort"
|
||||
"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 {
|
||||
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)
|
||||
}
|
||||
|
||||
signer := &httpSigner{
|
||||
Request: r,
|
||||
PayloadHash: payloadHash,
|
||||
ServiceName: service,
|
||||
RegionSet: regionSet,
|
||||
Credentials: credentials,
|
||||
Time: signingTime.UTC(),
|
||||
IsPreSign: isPresigned,
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -1,431 +0,0 @@
|
|||
// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/v4a_test.go
|
||||
|
||||
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 ""
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/cache.go
|
||||
|
||||
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
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
// 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
|
||||
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"
|
||||
)
|
|
@ -1,90 +0,0 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/header_rules.go
|
||||
|
||||
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)
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
// 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
|
||||
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-"},
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/header_test.go
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/hmac.go
|
||||
|
||||
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)
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/host.go
|
||||
|
||||
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
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/scope.go
|
||||
|
||||
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",
|
||||
}, "/")
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/time.go
|
||||
|
||||
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
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/util.go
|
||||
|
||||
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
|
||||
}
|
|
@ -1,160 +0,0 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/util_test.go
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/v4/stream.go
|
||||
|
||||
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")
|
||||
}
|
|
@ -1,578 +0,0 @@
|
|||
// 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).
|
||||
//
|
||||
// For more information about SigV4, see [Signing AWS API requests] in the IAM
|
||||
// user guide.
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// # Pre-escaping a request URI
|
||||
//
|
||||
// AWS v4 signature validation requires that the canonical string's URI path
|
||||
// component must be the escaped form of the HTTP request's path.
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// 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:
|
||||
//
|
||||
// "//<hostname>/<path>"
|
||||
//
|
||||
// // e.g.
|
||||
// "//example.com/some/path"
|
||||
//
|
||||
// The leading "//" and hostname are required or the escaping will not work
|
||||
// correctly.
|
||||
//
|
||||
// The TestStandaloneSign unit test 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 (
|
||||
"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{}
|
||||
|
||||
// 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 {
|
||||
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`
|
|
@ -1,365 +0,0 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/v4/v4_test.go
|
||||
|
||||
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 ""
|
||||
}
|
|
@ -73,7 +73,7 @@ func (h *handler) GetObjectAttributesHandler(w http.ResponseWriter, r *http.Requ
|
|||
ctx := r.Context()
|
||||
reqInfo := middleware.GetReqInfo(ctx)
|
||||
|
||||
params, err := parseGetObjectAttributeArgs(r)
|
||||
params, err := parseGetObjectAttributeArgs(r, h.reqLogger(ctx))
|
||||
if err != nil {
|
||||
h.logAndSendError(ctx, w, "invalid request", reqInfo, err)
|
||||
return
|
||||
|
@ -145,7 +145,7 @@ func writeAttributesHeaders(h http.Header, info *data.ExtendedObjectInfo, isBuck
|
|||
// x-amz-request-charged
|
||||
}
|
||||
|
||||
func parseGetObjectAttributeArgs(r *http.Request) (*GetObjectAttributesArgs, error) {
|
||||
func parseGetObjectAttributeArgs(r *http.Request, log *zap.Logger) (*GetObjectAttributesArgs, error) {
|
||||
res := &GetObjectAttributesArgs{
|
||||
VersionID: r.URL.Query().Get(api.QueryVersionID),
|
||||
}
|
||||
|
@ -178,8 +178,8 @@ func parseGetObjectAttributeArgs(r *http.Request) (*GetObjectAttributesArgs, err
|
|||
}
|
||||
}
|
||||
|
||||
res.Conditional, err = parseConditionalHeaders(r.Header)
|
||||
return res, err
|
||||
res.Conditional = parseConditionalHeaders(r.Header, log)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func encodeToObjectAttributesResponse(info *data.ObjectInfo, p *GetObjectAttributesArgs, md5Enabled bool) (*GetObjectAttributesResponse, error) {
|
||||
|
|
|
@ -3,7 +3,6 @@ package handler
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
|
@ -15,6 +14,9 @@ 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"
|
||||
)
|
||||
|
||||
|
@ -137,17 +139,16 @@ func TestDeleteObjectsError(t *testing.T) {
|
|||
hc.tp.SetObjectError(addr, expectedError)
|
||||
|
||||
w := deleteObjectsBase(hc, bktName, [][2]string{{objName, nodeVersion.OID.EncodeToString()}})
|
||||
var buf bytes.Buffer
|
||||
res := &DeleteObjectsResponse{}
|
||||
err = xml.NewDecoder(io.TeeReader(w.Result().Body, &buf)).Decode(res)
|
||||
|
||||
res := &s3.DeleteObjectsOutput{}
|
||||
err = xmlutil.UnmarshalXML(res, xml.NewDecoder(w.Result().Body), "")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Contains(t, buf.String(), "VersionId")
|
||||
require.ElementsMatch(t, []DeleteError{{
|
||||
Code: expectedError.Code,
|
||||
Key: objName,
|
||||
Message: expectedError.Error(),
|
||||
VersionID: nodeVersion.OID.EncodeToString(),
|
||||
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()),
|
||||
}}, res.Errors)
|
||||
}
|
||||
|
||||
|
|
|
@ -46,6 +46,10 @@ func TestSimpleGetEncrypted(t *testing.T) {
|
|||
|
||||
response, _ := getEncryptedObject(tc, bktName, objName)
|
||||
require.Equal(t, content, string(response))
|
||||
|
||||
result := listVersions(t, tc, bktName)
|
||||
require.Len(t, result.Version, 1)
|
||||
require.Equal(t, uint64(len(content)), result.Version[0].Size)
|
||||
}
|
||||
|
||||
func TestMD5HeaderBadOrEmpty(t *testing.T) {
|
||||
|
@ -369,6 +373,10 @@ func TestMultipartEncrypted(t *testing.T) {
|
|||
|
||||
part2Range := getEncryptedObjectRange(t, hc, bktName, objName, len(part1), len(part1)+len(part2)-1)
|
||||
require.Equal(t, part2[0:], part2Range)
|
||||
|
||||
result := listVersions(t, hc, bktName)
|
||||
require.Len(t, result.Version, 1)
|
||||
require.EqualValues(t, uint64(partSize+5), result.Version[0].Size)
|
||||
}
|
||||
|
||||
func putEncryptedObject(t *testing.T, tc *handlerContext, bktName, objName, content string) {
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"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/internal/logs"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
|
@ -154,11 +155,7 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
reqInfo = middleware.GetReqInfo(ctx)
|
||||
)
|
||||
|
||||
conditional, err := parseConditionalHeaders(r.Header)
|
||||
if err != nil {
|
||||
h.logAndSendError(ctx, w, "could not parse request params", reqInfo, err)
|
||||
return
|
||||
}
|
||||
conditional := parseConditionalHeaders(r.Header, h.reqLogger(ctx))
|
||||
|
||||
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
||||
if err != nil {
|
||||
|
@ -290,21 +287,24 @@ func checkPreconditions(info *data.ObjectInfo, args *conditionalArgs, md5Enabled
|
|||
return nil
|
||||
}
|
||||
|
||||
func parseConditionalHeaders(headers http.Header) (*conditionalArgs, error) {
|
||||
var err error
|
||||
func parseConditionalHeaders(headers http.Header, log *zap.Logger) *conditionalArgs {
|
||||
args := &conditionalArgs{
|
||||
IfMatch: data.UnQuote(headers.Get(api.IfMatch)),
|
||||
IfNoneMatch: data.UnQuote(headers.Get(api.IfNoneMatch)),
|
||||
}
|
||||
|
||||
if args.IfModifiedSince, err = parseHTTPTime(headers.Get(api.IfModifiedSince)); err != nil {
|
||||
return nil, err
|
||||
if httpTime, err := parseHTTPTime(headers.Get(api.IfModifiedSince)); err == nil {
|
||||
args.IfModifiedSince = httpTime
|
||||
} else {
|
||||
log.Warn(logs.FailedToParseHTTPTime, zap.String(api.IfModifiedSince, headers.Get(api.IfModifiedSince)), zap.Error(err))
|
||||
}
|
||||
if args.IfUnmodifiedSince, err = parseHTTPTime(headers.Get(api.IfUnmodifiedSince)); err != nil {
|
||||
return nil, err
|
||||
if httpTime, err := parseHTTPTime(headers.Get(api.IfUnmodifiedSince)); err == nil {
|
||||
args.IfUnmodifiedSince = httpTime
|
||||
} else {
|
||||
log.Warn(logs.FailedToParseHTTPTime, zap.String(api.IfUnmodifiedSince, headers.Get(api.IfUnmodifiedSince)), zap.Error(err))
|
||||
}
|
||||
|
||||
return args, nil
|
||||
return args
|
||||
}
|
||||
|
||||
func parseHTTPTime(data string) (*time.Time, error) {
|
||||
|
|
|
@ -36,11 +36,7 @@ func (h *handler) HeadObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
conditional, err := parseConditionalHeaders(r.Header)
|
||||
if err != nil {
|
||||
h.logAndSendError(ctx, w, "could not parse request params", reqInfo, err)
|
||||
return
|
||||
}
|
||||
conditional := parseConditionalHeaders(r.Header, h.reqLogger(ctx))
|
||||
|
||||
p := &layer.HeadObjectParams{
|
||||
BktInfo: bktInfo,
|
||||
|
|
|
@ -68,6 +68,16 @@ func TestConditionalHead(t *testing.T) {
|
|||
api.IfModifiedSince: zeroTime.UTC().Format(http.TimeFormat),
|
||||
}
|
||||
headObject(t, tc, bktName, objName, headers, http.StatusNotModified)
|
||||
|
||||
headers = map[string]string{
|
||||
api.IfUnmodifiedSince: zeroTime.UTC().Format(time.RFC3339), // invalid format, header is ignored
|
||||
}
|
||||
headObject(t, tc, bktName, objName, headers, http.StatusOK)
|
||||
|
||||
headers = map[string]string{
|
||||
api.IfModifiedSince: objInfo.Created.Add(time.Minute).Format(time.RFC3339), // invalid format, header is ignored
|
||||
}
|
||||
headObject(t, tc, bktName, objName, headers, http.StatusOK)
|
||||
}
|
||||
|
||||
func headObject(t *testing.T, tc *handlerContext, bktName, objName string, headers map[string]string, status int) {
|
||||
|
|
|
@ -382,6 +382,10 @@ func TestMultipartUploadSize(t *testing.T) {
|
|||
attr := getObjectAttributes(hc, newBucket, newObjName, objectParts)
|
||||
require.Equal(t, 1, attr.ObjectParts.PartsCount)
|
||||
require.Equal(t, srcObjInfo.Headers[layer.AttributeDecryptedSize], strconv.Itoa(attr.ObjectParts.Parts[0].Size))
|
||||
|
||||
result := listVersions(t, hc, bktName)
|
||||
require.Len(t, result.Version, 1)
|
||||
require.EqualValues(t, objLen, result.Version[0].Size)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"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/internal/logs"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
|
@ -31,11 +32,7 @@ func (h *handler) PatchObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
conditional, err := parsePatchConditionalHeaders(r.Header)
|
||||
if err != nil {
|
||||
h.logAndSendError(ctx, w, "could not parse conditional headers", reqInfo, err)
|
||||
return
|
||||
}
|
||||
conditional := parsePatchConditionalHeaders(r.Header, h.reqLogger(ctx))
|
||||
|
||||
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
||||
if err != nil {
|
||||
|
@ -137,17 +134,18 @@ func (h *handler) PatchObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func parsePatchConditionalHeaders(headers http.Header) (*conditionalArgs, error) {
|
||||
var err error
|
||||
func parsePatchConditionalHeaders(headers http.Header, log *zap.Logger) *conditionalArgs {
|
||||
args := &conditionalArgs{
|
||||
IfMatch: data.UnQuote(headers.Get(api.IfMatch)),
|
||||
}
|
||||
|
||||
if args.IfUnmodifiedSince, err = parseHTTPTime(headers.Get(api.IfUnmodifiedSince)); err != nil {
|
||||
return nil, err
|
||||
if httpTime, err := parseHTTPTime(headers.Get(api.IfUnmodifiedSince)); err == nil {
|
||||
args.IfUnmodifiedSince = httpTime
|
||||
} else {
|
||||
log.Warn(logs.FailedToParseHTTPTime, zap.String(api.IfUnmodifiedSince, headers.Get(api.IfUnmodifiedSince)), zap.Error(err))
|
||||
}
|
||||
|
||||
return args, nil
|
||||
return args
|
||||
}
|
||||
|
||||
func parsePatchByteRange(rangeStr string, objSize uint64) (*layer.RangeParams, error) {
|
||||
|
|
|
@ -60,6 +60,13 @@ func TestPatch(t *testing.T) {
|
|||
api.IfMatch: etag,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "If-Unmodified-Since invalid format, header is ignored",
|
||||
rng: "bytes 0-2/*",
|
||||
headers: map[string]string{
|
||||
api.IfUnmodifiedSince: created.Add(-24 * time.Hour).Format(time.RFC3339),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid range syntax",
|
||||
rng: "bytes 0-2",
|
||||
|
|
|
@ -312,8 +312,7 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func (h *handler) getBodyReader(r *http.Request) (io.ReadCloser, error) {
|
||||
shaType, streaming := api.IsSignedStreamingV4(r)
|
||||
if !streaming {
|
||||
if !api.IsSignedStreamingV4(r) {
|
||||
return r.Body, nil
|
||||
}
|
||||
|
||||
|
@ -346,16 +345,7 @@ 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())
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
chunkReader io.ReadCloser
|
||||
)
|
||||
if shaType == api.StreamingContentV4aSHA256 {
|
||||
chunkReader, err = newSignV4aChunkedReader(r)
|
||||
} else {
|
||||
chunkReader, err = newSignV4ChunkedReader(r)
|
||||
}
|
||||
|
||||
chunkReader, err := newSignV4ChunkedReader(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("initialize chunk reader: %w", err)
|
||||
}
|
||||
|
|
|
@ -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/v4sdk2/signer/v4"
|
||||
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/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-v2/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"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 := aws.Credentials{AccessKeyID: AWSAccessKeyID, SecretAccessKey: AWSSecretAccessKey}
|
||||
signer := v4.NewSigner()
|
||||
awsCreds := credentials.NewStaticCredentials(AWSAccessKeyID, AWSSecretAccessKey, "")
|
||||
signer := v4.NewSigner(awsCreds)
|
||||
|
||||
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.SignHTTP(ctx, awsCreds, req, auth.UnsignedPayload, "s3", "us-east-1", signTime)
|
||||
_, err = signer.Sign(req, nil, "s3", "us-east-1", signTime)
|
||||
require.NoError(t, err)
|
||||
|
||||
req.Body = io.NopCloser(reqBody)
|
||||
|
|
|
@ -3,17 +3,16 @@ package handler
|
|||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/v4"
|
||||
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/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-v2/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -23,7 +22,6 @@ const (
|
|||
|
||||
type (
|
||||
s3ChunkReader struct {
|
||||
ctx context.Context
|
||||
reader *bufio.Reader
|
||||
streamSigner *v4.StreamSigner
|
||||
|
||||
|
@ -168,7 +166,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(c.ctx, nil, c.buffer, c.requestTime)
|
||||
calculatedSignature, err := c.streamSigner.GetSignature(nil, c.buffer, c.requestTime)
|
||||
if err != nil {
|
||||
c.err = err
|
||||
return num, c.err
|
||||
|
@ -191,31 +189,29 @@ func (c *s3ChunkReader) Read(buf []byte) (num int, err error) {
|
|||
}
|
||||
|
||||
func newSignV4ChunkedReader(req *http.Request) (io.ReadCloser, error) {
|
||||
ctx := req.Context()
|
||||
box, err := middleware.GetBoxData(ctx)
|
||||
box, err := middleware.GetBoxData(req.Context())
|
||||
if err != nil {
|
||||
return nil, errs.GetAPIError(errs.ErrAuthorizationHeaderMalformed)
|
||||
}
|
||||
|
||||
authHeaders, err := middleware.GetAuthHeaders(ctx)
|
||||
authHeaders, err := middleware.GetAuthHeaders(req.Context())
|
||||
if err != nil {
|
||||
return nil, errs.GetAPIError(errs.ErrAuthorizationHeaderMalformed)
|
||||
}
|
||||
|
||||
currentCredentials := aws.Credentials{AccessKeyID: authHeaders.AccessKeyID, SecretAccessKey: box.Gate.SecretKey}
|
||||
currentCredentials := credentials.NewStaticCredentials(authHeaders.AccessKeyID, box.Gate.SecretKey, "")
|
||||
seed, err := hex.DecodeString(authHeaders.SignatureV4)
|
||||
if err != nil {
|
||||
return nil, errs.GetAPIError(errs.ErrSignatureDoesNotMatch)
|
||||
}
|
||||
|
||||
reqTime, err := middleware.GetClientTime(ctx)
|
||||
reqTime, err := middleware.GetClientTime(req.Context())
|
||||
if err != nil {
|
||||
return nil, errs.GetAPIError(errs.ErrMalformedDate)
|
||||
}
|
||||
newStreamSigner := v4.NewStreamSigner(currentCredentials, "s3", authHeaders.Region, seed)
|
||||
newStreamSigner := v4.NewStreamSigner(authHeaders.Region, "s3", seed, currentCredentials)
|
||||
|
||||
return &s3ChunkReader{
|
||||
ctx: ctx,
|
||||
reader: bufio.NewReader(req.Body),
|
||||
streamSigner: newStreamSigner,
|
||||
requestTime: reqTime,
|
||||
|
|
|
@ -1,209 +0,0 @@
|
|||
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=" + <signature-as-hex> + "**\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
|
||||
}
|
|
@ -94,8 +94,7 @@ const (
|
|||
|
||||
DefaultLocationConstraint = "default"
|
||||
|
||||
StreamingContentSHA256 = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
|
||||
StreamingContentV4aSHA256 = "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD"
|
||||
StreamingContentSHA256 = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
|
||||
|
||||
DefaultStorageClass = "STANDARD"
|
||||
)
|
||||
|
@ -126,9 +125,7 @@ var SystemMetadata = map[string]struct{}{
|
|||
ContentLanguage: {},
|
||||
}
|
||||
|
||||
func IsSignedStreamingV4(r *http.Request) (string, bool) {
|
||||
shaHeader := r.Header.Get(AmzContentSha256)
|
||||
return shaHeader,
|
||||
(shaHeader == StreamingContentSHA256 || shaHeader == StreamingContentV4aSHA256) &&
|
||||
r.Method == http.MethodPut
|
||||
func IsSignedStreamingV4(r *http.Request) bool {
|
||||
return r.Header.Get(AmzContentSha256) == StreamingContentSHA256 &&
|
||||
r.Method == http.MethodPut
|
||||
}
|
||||
|
|
|
@ -224,8 +224,12 @@ func (n *Layer) uploadPart(ctx context.Context, multipartInfo *data.MultipartInf
|
|||
}
|
||||
|
||||
decSize := p.Size
|
||||
md5Hash := md5.New()
|
||||
if p.Info.Encryption.Enabled() {
|
||||
r, encSize, err := encryptionReader(p.Reader, p.Size, p.Info.Encryption.Key())
|
||||
rr := wrapReader(p.Reader, 64*1024, func(buf []byte) {
|
||||
md5Hash.Write(buf)
|
||||
})
|
||||
r, encSize, err := encryptionReader(rr, p.Size, p.Info.Encryption.Key())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create ecnrypted reader: %w", err)
|
||||
}
|
||||
|
@ -246,7 +250,12 @@ func (n *Layer) uploadPart(ctx context.Context, multipartInfo *data.MultipartInf
|
|||
if err != nil {
|
||||
return nil, apierr.GetAPIError(apierr.ErrInvalidDigest)
|
||||
}
|
||||
if hex.EncodeToString(hashBytes) != hex.EncodeToString(createdObj.MD5Sum) {
|
||||
|
||||
match := bytes.Equal(hashBytes, createdObj.MD5Sum)
|
||||
if p.Info.Encryption.Enabled() {
|
||||
match = bytes.Equal(hashBytes, md5Hash.Sum(nil))
|
||||
}
|
||||
if !match {
|
||||
prm := frostfs.PrmObjectDelete{
|
||||
Object: createdObj.ID,
|
||||
Container: bktInfo.CID,
|
||||
|
@ -388,7 +397,6 @@ func (n *Layer) CompleteMultipartUpload(ctx context.Context, p *CompleteMultipar
|
|||
}
|
||||
|
||||
var multipartObjetSize uint64
|
||||
var encMultipartObjectSize uint64
|
||||
parts := make([]*data.PartInfoExtended, 0, len(p.Parts))
|
||||
|
||||
var completedPartsHeader strings.Builder
|
||||
|
@ -407,11 +415,10 @@ func (n *Layer) CompleteMultipartUpload(ctx context.Context, p *CompleteMultipar
|
|||
multipartObjetSize += partInfo.Size // even if encryption is enabled size is actual (decrypted)
|
||||
|
||||
if encInfo.Enabled {
|
||||
encPartSize, err := sio.EncryptedSize(partInfo.Size)
|
||||
_, err := sio.EncryptedSize(partInfo.Size)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("compute encrypted size: %w", err)
|
||||
}
|
||||
encMultipartObjectSize += encPartSize
|
||||
}
|
||||
|
||||
partInfoStr := partInfo.ToHeaderString()
|
||||
|
@ -449,7 +456,6 @@ func (n *Layer) CompleteMultipartUpload(ctx context.Context, p *CompleteMultipar
|
|||
initMetadata[AttributeHMACKey] = encInfo.HMACKey
|
||||
initMetadata[AttributeHMACSalt] = encInfo.HMACSalt
|
||||
initMetadata[AttributeDecryptedSize] = strconv.FormatUint(multipartObjetSize, 10)
|
||||
multipartObjetSize = encMultipartObjectSize
|
||||
}
|
||||
|
||||
partsData, err := json.Marshal(parts)
|
||||
|
|
|
@ -243,11 +243,9 @@ func (n *Layer) PutObject(ctx context.Context, p *PutObjectParams) (*data.Extend
|
|||
return nil, fmt.Errorf("add encryption header: %w", err)
|
||||
}
|
||||
|
||||
var encSize uint64
|
||||
if r, encSize, err = encryptionReader(p.Reader, size, p.Encryption.Key()); err != nil {
|
||||
if r, _, err = encryptionReader(p.Reader, size, p.Encryption.Key()); err != nil {
|
||||
return nil, fmt.Errorf("create encrypter: %w", err)
|
||||
}
|
||||
p.Size = &encSize
|
||||
}
|
||||
|
||||
if r != nil {
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
package modules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
@ -39,7 +38,6 @@ const (
|
|||
awsAccessKeyIDFlag = "aws-access-key-id"
|
||||
awsSecretAccessKeyFlag = "aws-secret-access-key"
|
||||
headerFlag = "header"
|
||||
sigV4AFlag = "sigv4a"
|
||||
)
|
||||
|
||||
func initGeneratePresignedURLCmd() {
|
||||
|
@ -53,53 +51,35 @@ 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)
|
||||
_ = generatePresignedURLCmd.MarkFlagRequired(objectFlag)
|
||||
}
|
||||
|
||||
func runGeneratePresignedURLCmd(cmd *cobra.Command, _ []string) error {
|
||||
ctx, cancel := context.WithTimeout(cmd.Context(), viper.GetDuration(timeoutFlag))
|
||||
defer cancel()
|
||||
func runGeneratePresignedURLCmd(*cobra.Command, []string) error {
|
||||
var cfg aws.Config
|
||||
|
||||
log := getLogger()
|
||||
if region := viper.GetString(regionFlag); region != "" {
|
||||
cfg.Region = ®ion
|
||||
}
|
||||
accessKeyID := viper.GetString(awsAccessKeyIDFlag)
|
||||
secretAccessKey := viper.GetString(awsSecretAccessKeyFlag)
|
||||
|
||||
var (
|
||||
region string
|
||||
creds aws.Credentials
|
||||
)
|
||||
|
||||
profile := viper.GetString(profileFlag)
|
||||
|
||||
if profile == "" {
|
||||
cfg, err := config.LoadDefaultConfig(ctx)
|
||||
if err != nil {
|
||||
return wrapPreparationError(err)
|
||||
}
|
||||
region = cfg.Region
|
||||
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(ctx, 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
|
||||
if accessKeyID != "" && secretAccessKey != "" {
|
||||
cfg.Credentials = credentials.NewStaticCredentialsFromCreds(credentials.Value{
|
||||
AccessKeyID: accessKeyID,
|
||||
SecretAccessKey: secretAccessKey,
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
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))
|
||||
}
|
||||
|
||||
reqData := auth.RequestData{
|
||||
|
@ -110,7 +90,7 @@ func runGeneratePresignedURLCmd(cmd *cobra.Command, _ []string) error {
|
|||
}
|
||||
presignData := auth.PresignData{
|
||||
Service: "s3",
|
||||
Region: region,
|
||||
Region: *sess.Config.Region,
|
||||
Lifetime: viper.GetDuration(lifetimeFlag),
|
||||
SignTime: time.Now().UTC(),
|
||||
}
|
||||
|
@ -121,12 +101,7 @@ func runGeneratePresignedURLCmd(cmd *cobra.Command, _ []string) error {
|
|||
}
|
||||
presignData.Headers = headers
|
||||
|
||||
var req *http.Request
|
||||
if viper.GetBool(sigV4AFlag) {
|
||||
req, err = auth.PresignRequestV4a(creds, reqData, presignData, log)
|
||||
} else {
|
||||
req, err = auth.PresignRequest(ctx, creds, reqData, presignData, log)
|
||||
}
|
||||
req, err := auth.PresignRequest(sess.Config.Credentials, reqData, presignData)
|
||||
if err != nil {
|
||||
return wrapBusinessLogicError(err)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package modules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
@ -9,8 +8,9 @@ import (
|
|||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
@ -25,8 +25,7 @@ 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 --sigv4a`,
|
||||
frostfs-s3-authmate sign --data some-data --region ru --service s3 --time 2024-09-27 --aws-access-key-id ETaA2CadPcA7bAkLsML2PbTudXY8uRt2PDjCCwkvRv9s0FDCxWDXYc1SA1vKv8KbyCNsLY2AmAjJ92Vz5rgvsFCy --aws-secret-access-key c2d65ef2980f03f4f495bdebedeeae760496697880d61d106bb9a4e5cd2e0607`,
|
||||
RunE: runSignCmd,
|
||||
}
|
||||
|
||||
|
@ -44,47 +43,35 @@ 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 {
|
||||
ctx, cancel := context.WithTimeout(cmd.Context(), viper.GetDuration(timeoutFlag))
|
||||
defer cancel()
|
||||
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 profile := viper.GetString(profileFlag); profile == "" {
|
||||
cfg, err := config.LoadDefaultConfig(ctx)
|
||||
if err != nil {
|
||||
return wrapPreparationError(err)
|
||||
}
|
||||
region = cfg.Region
|
||||
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(ctx, profile)
|
||||
if err != nil {
|
||||
return wrapPreparationError(fmt.Errorf("couldn't get '%s' aws credentials: %w", profile, err))
|
||||
}
|
||||
region = cfg.Region
|
||||
creds = cfg.Credentials
|
||||
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))
|
||||
}
|
||||
|
||||
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
|
||||
sess, err := session.NewSessionWithOptions(session.Options{
|
||||
Config: cfg,
|
||||
Profile: viper.GetString(profileFlag),
|
||||
SharedConfigState: session.SharedConfigEnable,
|
||||
})
|
||||
if err != nil {
|
||||
return wrapPreparationError(fmt.Errorf("couldn't get aws credentials: %w", err))
|
||||
}
|
||||
|
||||
data := viper.GetString(dataFlag)
|
||||
|
@ -96,6 +83,15 @@ 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"))
|
||||
|
@ -106,23 +102,11 @@ func runSignCmd(cmd *cobra.Command, _ []string) error {
|
|||
signTime = time.Now()
|
||||
}
|
||||
|
||||
var signature string
|
||||
sigv4a := viper.GetBool(sigV4AFlag)
|
||||
if sigv4a {
|
||||
var err error
|
||||
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)
|
||||
}
|
||||
|
||||
if !sigv4a {
|
||||
cmd.Println("service:", service)
|
||||
cmd.Println("region:", region)
|
||||
cmd.Println("time:", signTime.UTC().Format("20060102"))
|
||||
}
|
||||
signature := auth.SignStr(creds.SecretAccessKey, service, *sess.Config.Region, signTime, data)
|
||||
|
||||
cmd.Println("service:", service)
|
||||
cmd.Println("region:", *sess.Config.Region)
|
||||
cmd.Println("time:", signTime.UTC().Format("20060102"))
|
||||
cmd.Println("accessKeyId:", creds.AccessKeyID)
|
||||
cmd.Printf("secretAccessKey: [****************%s]\n", creds.SecretAccessKey[max(0, len(creds.SecretAccessKey)-4):])
|
||||
cmd.Println("signature:", signature)
|
||||
|
|
18
go.mod
18
go.mod
|
@ -10,10 +10,9 @@ 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-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/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/bluele/gcache v0.0.2
|
||||
github.com/go-chi/chi/v5 v5.0.8
|
||||
github.com/google/uuid v1.6.0
|
||||
|
@ -50,15 +49,7 @@ 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/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/aws/smithy-go v1.20.3 // 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
|
||||
|
@ -75,6 +66,7 @@ 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
|
||||
|
|
43
go.sum
43
go.sum
|
@ -64,32 +64,14 @@ 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-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/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/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=
|
||||
|
@ -218,6 +200,10 @@ 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=
|
||||
|
@ -441,6 +427,7 @@ 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=
|
||||
|
@ -504,11 +491,13 @@ 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=
|
||||
|
@ -517,6 +506,7 @@ 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=
|
||||
|
@ -687,6 +677,7 @@ 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=
|
||||
|
|
|
@ -177,4 +177,5 @@ const (
|
|||
MultinetConfigWontBeUpdated = "multinet config won't be updated"
|
||||
MultinetDialSuccess = "multinet dial successful"
|
||||
MultinetDialFail = "multinet dial failed"
|
||||
FailedToParseHTTPTime = "failed to parse http time, header is ignored"
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue