forked from TrueCloudLab/frostfs-s3-gw
[#339] Add aws-sdk-go-v2
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
parent
8f7ccb0f62
commit
cc9a68401f
48 changed files with 5894 additions and 51 deletions
|
@ -9,29 +9,39 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4"
|
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4"
|
||||||
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||||
|
v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||||
|
credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||||
|
"github.com/aws/smithy-go/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthorizationFieldRegexp -- is regexp for credentials with Base58 encoded cid and oid and '0' (zero) as delimiter.
|
var (
|
||||||
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>.+)`)
|
// 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>.+)`)
|
||||||
|
|
||||||
// postPolicyCredentialRegexp -- is regexp for credentials when uploading file using POST with policy.
|
// authorizationFieldV4aRegexp -- is regexp for credentials with Base58 encoded cid and oid and '0' (zero) as delimiter.
|
||||||
var postPolicyCredentialRegexp = regexp.MustCompile(`(?P<access_key_id>[^/]+)/(?P<date>[^/]+)/(?P<region>[^/]*)/(?P<service>[^/]+)/aws4_request`)
|
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`)
|
||||||
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Center struct {
|
Center struct {
|
||||||
reg *RegexpSubmatcher
|
reg *RegexpSubmatcher
|
||||||
|
regV4a *RegexpSubmatcher
|
||||||
postReg *RegexpSubmatcher
|
postReg *RegexpSubmatcher
|
||||||
cli tokens.Credentials
|
cli tokens.Credentials
|
||||||
allowedAccessKeyIDPrefixes []string // empty slice means all access key ids are allowed
|
allowedAccessKeyIDPrefixes []string // empty slice means all access key ids are allowed
|
||||||
|
@ -47,22 +57,26 @@ type (
|
||||||
AccessKeyID string
|
AccessKeyID string
|
||||||
Service string
|
Service string
|
||||||
Region string
|
Region string
|
||||||
SignatureV4 string
|
Signature string
|
||||||
SignedFields []string
|
SignedFields []string
|
||||||
Date string
|
Date string
|
||||||
IsPresigned bool
|
IsPresigned bool
|
||||||
Expiration time.Duration
|
Expiration time.Duration
|
||||||
|
Preamble string
|
||||||
|
PayloadHash string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
authHeaderPartsNum = 6
|
authHeaderPartsNum = 6
|
||||||
maxFormSizeMemory = 50 * 1048576 // 50 MB
|
authHeaderV4aPartsNum = 5
|
||||||
|
maxFormSizeMemory = 50 * 1048576 // 50 MB
|
||||||
|
|
||||||
AmzAlgorithm = "X-Amz-Algorithm"
|
AmzAlgorithm = "X-Amz-Algorithm"
|
||||||
AmzCredential = "X-Amz-Credential"
|
AmzCredential = "X-Amz-Credential"
|
||||||
AmzSignature = "X-Amz-Signature"
|
AmzSignature = "X-Amz-Signature"
|
||||||
AmzSignedHeaders = "X-Amz-SignedHeaders"
|
AmzSignedHeaders = "X-Amz-SignedHeaders"
|
||||||
|
AmzRegionSet = "X-Amz-Region-Set"
|
||||||
AmzExpires = "X-Amz-Expires"
|
AmzExpires = "X-Amz-Expires"
|
||||||
AmzDate = "X-Amz-Date"
|
AmzDate = "X-Amz-Date"
|
||||||
AmzContentSHA256 = "X-Amz-Content-Sha256"
|
AmzContentSHA256 = "X-Amz-Content-Sha256"
|
||||||
|
@ -91,27 +105,56 @@ func New(creds tokens.Credentials, prefixes []string, settings CenterSettings) *
|
||||||
return &Center{
|
return &Center{
|
||||||
cli: creds,
|
cli: creds,
|
||||||
reg: NewRegexpMatcher(AuthorizationFieldRegexp),
|
reg: NewRegexpMatcher(AuthorizationFieldRegexp),
|
||||||
|
regV4a: NewRegexpMatcher(authorizationFieldV4aRegexp),
|
||||||
postReg: NewRegexpMatcher(postPolicyCredentialRegexp),
|
postReg: NewRegexpMatcher(postPolicyCredentialRegexp),
|
||||||
allowedAccessKeyIDPrefixes: prefixes,
|
allowedAccessKeyIDPrefixes: prefixes,
|
||||||
settings: settings,
|
settings: settings,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Center) parseAuthHeader(header string) (*AuthHeader, error) {
|
const (
|
||||||
submatches := c.reg.GetSubmatches(header)
|
signaturePreambleSigV4 = "AWS4-HMAC-SHA256"
|
||||||
if len(submatches) != authHeaderPartsNum {
|
signaturePreambleSigV4A = "AWS4-ECDSA-P256-SHA256"
|
||||||
return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrAuthorizationHeaderMalformed), header)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
signedFields := strings.Split(submatches["signed_header_fields"], ";")
|
func (c *Center) parseAuthHeader(authHeader string, headers http.Header) (*AuthHeader, error) {
|
||||||
|
preamble, _, _ := strings.Cut(authHeader, " ")
|
||||||
|
|
||||||
|
var (
|
||||||
|
submatches map[string]string
|
||||||
|
region string
|
||||||
|
)
|
||||||
|
|
||||||
|
switch preamble {
|
||||||
|
case signaturePreambleSigV4:
|
||||||
|
submatches = c.reg.GetSubmatches(authHeader)
|
||||||
|
if len(submatches) != authHeaderPartsNum {
|
||||||
|
return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrAuthorizationHeaderMalformed), authHeader)
|
||||||
|
}
|
||||||
|
region = submatches["region"]
|
||||||
|
case signaturePreambleSigV4A:
|
||||||
|
submatches = c.regV4a.GetSubmatches(authHeader)
|
||||||
|
if len(submatches) != authHeaderV4aPartsNum {
|
||||||
|
return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrAuthorizationHeaderMalformed), authHeader)
|
||||||
|
}
|
||||||
|
region = headers.Get(AmzRegionSet)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrAuthorizationHeaderMalformed), authHeader)
|
||||||
|
}
|
||||||
|
// AWS4-ECDSA-P256-SHA256
|
||||||
|
// Credential=2XGRML5EW3LMHdf64W2DkBy1Nkuu4y4wGhUj44QjbXBi05ZNvs8WVwy1XTmSEkcVkydPKzCgtmR7U3zyLYTj3Snxf/20240326/s3/aws4_request,
|
||||||
|
// SignedHeaders=amz-sdk-invocation-id;amz-sdk-request;host;x-amz-content-sha256;x-amz-date;x-amz-region-set,
|
||||||
|
// Signature=3044022006a2bc760140834101d0a79667d6aa75768c1a28e9cafc8963484d0752a6c6050220629dc06d7d6505e1b1e2a5d1f974b25ba32fdffc6f3f70dc4dda31b8a6f7ea2b
|
||||||
|
|
||||||
return &AuthHeader{
|
return &AuthHeader{
|
||||||
AccessKeyID: submatches["access_key_id"],
|
AccessKeyID: submatches["access_key_id"],
|
||||||
Service: submatches["service"],
|
Service: submatches["service"],
|
||||||
Region: submatches["region"],
|
Region: region,
|
||||||
SignatureV4: submatches["v4_signature"],
|
Signature: submatches["v4_signature"],
|
||||||
SignedFields: signedFields,
|
SignedFields: strings.Split(submatches["signed_header_fields"], ";"),
|
||||||
Date: submatches["date"],
|
Date: submatches["date"],
|
||||||
|
Preamble: preamble,
|
||||||
|
PayloadHash: headers.Get(AmzContentSHA256),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +172,7 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) {
|
||||||
)
|
)
|
||||||
|
|
||||||
queryValues := r.URL.Query()
|
queryValues := r.URL.Query()
|
||||||
if queryValues.Get(AmzAlgorithm) == "AWS4-HMAC-SHA256" {
|
if queryValues.Get(AmzAlgorithm) == signaturePreambleSigV4 {
|
||||||
creds := strings.Split(queryValues.Get(AmzCredential), "/")
|
creds := strings.Split(queryValues.Get(AmzCredential), "/")
|
||||||
if len(creds) != 5 || creds[4] != "aws4_request" {
|
if len(creds) != 5 || creds[4] != "aws4_request" {
|
||||||
return nil, fmt.Errorf("bad X-Amz-Credential")
|
return nil, fmt.Errorf("bad X-Amz-Credential")
|
||||||
|
@ -138,10 +181,31 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) {
|
||||||
AccessKeyID: creds[0],
|
AccessKeyID: creds[0],
|
||||||
Service: creds[3],
|
Service: creds[3],
|
||||||
Region: creds[2],
|
Region: creds[2],
|
||||||
SignatureV4: queryValues.Get(AmzSignature),
|
Signature: queryValues.Get(AmzSignature),
|
||||||
SignedFields: strings.Split(queryValues.Get(AmzSignedHeaders), ";"),
|
SignedFields: strings.Split(queryValues.Get(AmzSignedHeaders), ";"),
|
||||||
Date: creds[1],
|
Date: creds[1],
|
||||||
IsPresigned: true,
|
IsPresigned: true,
|
||||||
|
Preamble: signaturePreambleSigV4,
|
||||||
|
}
|
||||||
|
authHdr.Expiration, err = time.ParseDuration(queryValues.Get(AmzExpires) + "s")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("couldn't parse X-Amz-Expires: %w", err)
|
||||||
|
}
|
||||||
|
signatureDateTimeStr = queryValues.Get(AmzDate)
|
||||||
|
} else if queryValues.Get(AmzAlgorithm) == signaturePreambleSigV4A {
|
||||||
|
creds := strings.Split(queryValues.Get(AmzCredential), "/")
|
||||||
|
if len(creds) != 4 || creds[3] != "aws4_request" {
|
||||||
|
return nil, fmt.Errorf("bad X-Amz-Credential")
|
||||||
|
}
|
||||||
|
authHdr = &AuthHeader{
|
||||||
|
AccessKeyID: creds[0],
|
||||||
|
Service: creds[2],
|
||||||
|
Region: queryValues.Get(AmzRegionSet),
|
||||||
|
Signature: queryValues.Get(AmzSignature),
|
||||||
|
SignedFields: strings.Split(queryValues.Get(AmzSignedHeaders), ";"),
|
||||||
|
Date: creds[1],
|
||||||
|
IsPresigned: true,
|
||||||
|
Preamble: signaturePreambleSigV4A,
|
||||||
}
|
}
|
||||||
authHdr.Expiration, err = time.ParseDuration(queryValues.Get(AmzExpires) + "s")
|
authHdr.Expiration, err = time.ParseDuration(queryValues.Get(AmzExpires) + "s")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -156,7 +220,7 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) {
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("%w: %v", middleware.ErrNoAuthorizationHeader, authHeaderField)
|
return nil, fmt.Errorf("%w: %v", middleware.ErrNoAuthorizationHeader, authHeaderField)
|
||||||
}
|
}
|
||||||
authHdr, err = c.parseAuthHeader(authHeaderField[0])
|
authHdr, err = c.parseAuthHeader(authHeaderField[0], r.Header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -197,7 +261,7 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) {
|
||||||
AuthHeaders: &middleware.AuthHeader{
|
AuthHeaders: &middleware.AuthHeader{
|
||||||
AccessKeyID: authHdr.AccessKeyID,
|
AccessKeyID: authHdr.AccessKeyID,
|
||||||
Region: authHdr.Region,
|
Region: authHdr.Region,
|
||||||
SignatureV4: authHdr.SignatureV4,
|
SignatureV4: authHdr.Signature,
|
||||||
},
|
},
|
||||||
Attributes: attrs,
|
Attributes: attrs,
|
||||||
}
|
}
|
||||||
|
@ -331,37 +395,75 @@ func cloneRequest(r *http.Request, authHeader *AuthHeader) *http.Request {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Center) checkSign(authHeader *AuthHeader, box *accessbox.Box, request *http.Request, signatureDateTime time.Time) error {
|
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
|
var signature string
|
||||||
if authHeader.IsPresigned {
|
|
||||||
now := time.Now()
|
switch authHeader.Preamble {
|
||||||
if signatureDateTime.Add(authHeader.Expiration).Before(now) {
|
case signaturePreambleSigV4:
|
||||||
return fmt.Errorf("%w: expired: now %s, signature %s", apierr.GetAPIError(apierr.ErrExpiredPresignRequest),
|
awsCreds := credentials.NewStaticCredentials(authHeader.AccessKeyID, box.Gate.SecretKey, "")
|
||||||
now.Format(time.RFC3339), signatureDateTime.Format(time.RFC3339))
|
signer := v4.NewSigner(awsCreds)
|
||||||
|
signer.DisableURIPathEscaping = true
|
||||||
|
|
||||||
|
if authHeader.IsPresigned {
|
||||||
|
if err := checkPresignedDate(authHeader, signatureDateTime); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := signer.Presign(request, nil, authHeader.Service, authHeader.Region, authHeader.Expiration, signatureDateTime); err != nil {
|
||||||
|
return fmt.Errorf("failed to pre-sign temporary HTTP request: %w", err)
|
||||||
|
}
|
||||||
|
signature = request.URL.Query().Get(AmzSignature)
|
||||||
|
} else {
|
||||||
|
if _, err := signer.Sign(request, nil, authHeader.Service, authHeader.Region, signatureDateTime); err != nil {
|
||||||
|
return fmt.Errorf("failed to sign temporary HTTP request: %w", err)
|
||||||
|
}
|
||||||
|
signature = c.reg.GetSubmatches(request.Header.Get(AuthorizationHdr))["v4_signature"]
|
||||||
}
|
}
|
||||||
if now.Before(signatureDateTime) {
|
if authHeader.Signature != signature {
|
||||||
return fmt.Errorf("%w: signature time from the future: now %s, signature %s", apierr.GetAPIError(apierr.ErrBadRequest),
|
return fmt.Errorf("%w: %s != %s: headers %v", apierr.GetAPIError(apierr.ErrSignatureDoesNotMatch),
|
||||||
now.Format(time.RFC3339), signatureDateTime.Format(time.RFC3339))
|
authHeader.Signature, signature, authHeader.SignedFields)
|
||||||
}
|
}
|
||||||
if _, err := signer.Presign(request, nil, authHeader.Service, authHeader.Region, authHeader.Expiration, signatureDateTime); err != nil {
|
|
||||||
return fmt.Errorf("failed to pre-sign temporary HTTP request: %w", err)
|
case signaturePreambleSigV4A:
|
||||||
|
signer := v4a.NewSigner(func(options *v4a.SignerOptions) {
|
||||||
|
options.DisableURIPathEscaping = true
|
||||||
|
options.LogSigning = true
|
||||||
|
options.Logger = logging.NewStandardLogger(os.Stdout)
|
||||||
|
})
|
||||||
|
|
||||||
|
credAdapter := v4a.SymmetricCredentialAdaptor{
|
||||||
|
SymmetricProvider: credentialsv2.NewStaticCredentialsProvider(authHeader.AccessKeyID, box.Gate.SecretKey, ""),
|
||||||
}
|
}
|
||||||
signature = request.URL.Query().Get(AmzSignature)
|
|
||||||
} else {
|
creds, err := credAdapter.RetrievePrivateKey(request.Context())
|
||||||
if _, err := signer.Sign(request, nil, authHeader.Service, authHeader.Region, signatureDateTime); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to sign temporary HTTP request: %w", err)
|
return fmt.Errorf("failed to derive assymetric key from credentials: %w", err)
|
||||||
}
|
}
|
||||||
signature = c.reg.GetSubmatches(request.Header.Get(AuthorizationHdr))["v4_signature"]
|
|
||||||
|
if authHeader.IsPresigned {
|
||||||
|
if err = checkPresignedDate(authHeader, signatureDateTime); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return signer.VerifySignature(creds, request, authHeader.PayloadHash, authHeader.Service,
|
||||||
|
strings.Split(authHeader.Region, ","), signatureDateTime, authHeader.Signature)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid preamble: %s", authHeader.Preamble)
|
||||||
}
|
}
|
||||||
|
|
||||||
if authHeader.SignatureV4 != signature {
|
return nil
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
|
|
||||||
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4"
|
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache"
|
||||||
|
v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
|
||||||
|
@ -25,6 +26,8 @@ import (
|
||||||
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
|
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
|
||||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||||
|
credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.uber.org/zap/zaptest"
|
"go.uber.org/zap/zaptest"
|
||||||
)
|
)
|
||||||
|
@ -60,7 +63,7 @@ func TestAuthHeaderParse(t *testing.T) {
|
||||||
AccessKeyID: "oid0cid",
|
AccessKeyID: "oid0cid",
|
||||||
Service: "s3",
|
Service: "s3",
|
||||||
Region: "us-east-1",
|
Region: "us-east-1",
|
||||||
SignatureV4: "2811ccb9e242f41426738fb1f",
|
Signature: "2811ccb9e242f41426738fb1f",
|
||||||
SignedFields: []string{"host", "x-amz-content-sha256", "x-amz-date"},
|
SignedFields: []string{"host", "x-amz-content-sha256", "x-amz-date"},
|
||||||
Date: "20210809",
|
Date: "20210809",
|
||||||
},
|
},
|
||||||
|
@ -71,7 +74,7 @@ func TestAuthHeaderParse(t *testing.T) {
|
||||||
expected: nil,
|
expected: nil,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
authHeader, err := center.parseAuthHeader(tc.header)
|
authHeader, err := center.parseAuthHeader(tc.header, nil)
|
||||||
require.ErrorIs(t, err, tc.err, tc.header)
|
require.ErrorIs(t, err, tc.err, tc.header)
|
||||||
require.Equal(t, tc.expected, authHeader, tc.header)
|
require.Equal(t, tc.expected, authHeader, tc.header)
|
||||||
}
|
}
|
||||||
|
@ -90,6 +93,41 @@ func TestSignature(t *testing.T) {
|
||||||
require.Equal(t, "dfbe886241d9e369cf4b329ca0f15eb27306c97aa1022cc0bb5a914c4ef87634", signature)
|
require.Equal(t, "dfbe886241d9e369cf4b329ca0f15eb27306c97aa1022cc0bb5a914c4ef87634", signature)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSignatureV4A(t *testing.T) {
|
||||||
|
var accessKeyAddr oid.Address
|
||||||
|
err := accessKeyAddr.DecodeString("8N7CYBY74kxZXoyvA5UNdmovaXqFpwNfvEPsqaN81es2/3tDwq5tR8fByrJcyJwyiuYX7Dae8tyDT7pd8oaL1MBto")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
accessKeyID := strings.ReplaceAll(accessKeyAddr.String(), "/", "0")
|
||||||
|
secretKey := "713d0a0b9efc7d22923e17b0402a6a89b4273bc711c8bacb2da1b643d0006aeb"
|
||||||
|
|
||||||
|
signer := v4a.NewSigner(func(options *v4a.SignerOptions) {
|
||||||
|
options.DisableURIPathEscaping = true
|
||||||
|
})
|
||||||
|
|
||||||
|
credAdapter := v4a.SymmetricCredentialAdaptor{
|
||||||
|
SymmetricProvider: credentialsv2.NewStaticCredentialsProvider(accessKeyID, secretKey, ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", "http://localhost:8084/bucket/object", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
creds, err := credAdapter.RetrievePrivateKey(req.Context())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
signingTime := time.Now()
|
||||||
|
service := "s3"
|
||||||
|
regionSet := []string{"spb"}
|
||||||
|
|
||||||
|
err = signer.SignHTTP(req.Context(), creds, req, "", service, regionSet, signingTime)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
signature := NewRegexpMatcher(authorizationFieldV4aRegexp).GetSubmatches(req.Header.Get(AuthorizationHdr))["v4_signature"]
|
||||||
|
|
||||||
|
err = signer.VerifySignature(creds, req, "", service, regionSet, signingTime, signature)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestCheckFormatContentSHA256(t *testing.T) {
|
func TestCheckFormatContentSHA256(t *testing.T) {
|
||||||
defaultErr := errors.GetAPIError(errors.ErrContentSHA256Mismatch)
|
defaultErr := errors.GetAPIError(errors.ErrContentSHA256Mismatch)
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,17 @@ package auth
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4"
|
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4"
|
||||||
|
v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2"
|
||||||
|
credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||||
"github.com/aws/aws-sdk-go/private/protocol/rest"
|
"github.com/aws/aws-sdk-go/private/protocol/rest"
|
||||||
|
"github.com/aws/smithy-go/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RequestData struct {
|
type RequestData struct {
|
||||||
|
@ -49,3 +54,39 @@ func PresignRequest(creds *credentials.Credentials, reqData RequestData, presign
|
||||||
|
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PresignRequestV4a forms pre-signed request to access objects without aws credentials.
|
||||||
|
func PresignRequestV4a(credProvider credentialsv2.StaticCredentialsProvider, reqData RequestData, presignData PresignData) (*http.Request, error) {
|
||||||
|
urlStr := fmt.Sprintf("%s/%s/%s", reqData.Endpoint, rest.EscapePath(reqData.Bucket, false), rest.EscapePath(reqData.Object, false))
|
||||||
|
req, err := http.NewRequest(strings.ToUpper(reqData.Method), urlStr, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create new request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set(AmzDate, presignData.SignTime.Format("20060102T150405Z"))
|
||||||
|
req.Header.Set(ContentTypeHdr, "text/plain")
|
||||||
|
req.Header.Set(AmzExpires, strconv.Itoa(int(presignData.Lifetime.Seconds())))
|
||||||
|
|
||||||
|
signer := v4a.NewSigner(func(options *v4a.SignerOptions) {
|
||||||
|
options.DisableURIPathEscaping = true
|
||||||
|
options.LogSigning = true
|
||||||
|
options.Logger = logging.NewStandardLogger(os.Stdout)
|
||||||
|
})
|
||||||
|
|
||||||
|
credAdapter := v4a.SymmetricCredentialAdaptor{
|
||||||
|
SymmetricProvider: credProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
creds, err := credAdapter.RetrievePrivateKey(req.Context())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to derive assymetric key from credentials: %w", err)
|
||||||
|
}
|
||||||
|
presignedURL, _, err := signer.PresignHTTP(req.Context(), creds, req, "", presignData.Service, []string{presignData.Region}, presignData.SignTime)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("presign: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(presignedURL)
|
||||||
|
|
||||||
|
return http.NewRequest(reqData.Method, presignedURL, nil)
|
||||||
|
}
|
||||||
|
|
|
@ -2,17 +2,23 @@ package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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/accessbox"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||||
|
credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||||
|
"github.com/aws/smithy-go/logging"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -93,3 +99,93 @@ func TestCheckSign(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.EqualValues(t, expBox, box.AccessBox)
|
require.EqualValues(t, expBox, box.AccessBox)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCheckSignV4a(t *testing.T) {
|
||||||
|
var accessKeyAddr oid.Address
|
||||||
|
err := accessKeyAddr.DecodeString("8N7CYBY74kxZXoyvA5UNdmovaXqFpwNfvEPsqaN81es2/3tDwq5tR8fByrJcyJwyiuYX7Dae8tyDT7pd8oaL1MBto")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
accessKeyID := strings.ReplaceAll(accessKeyAddr.String(), "/", "0")
|
||||||
|
secretKey := "713d0a0b9efc7d22923e17b0402a6a89b4273bc711c8bacb2da1b643d0006aeb"
|
||||||
|
awsCreds := credentialsv2.NewStaticCredentialsProvider(accessKeyID, secretKey, "")
|
||||||
|
|
||||||
|
reqData := RequestData{
|
||||||
|
Method: "GET",
|
||||||
|
Endpoint: "http://localhost:8084",
|
||||||
|
Bucket: "my-bucket",
|
||||||
|
Object: "@obj/name",
|
||||||
|
}
|
||||||
|
presignData := PresignData{
|
||||||
|
Service: "s3",
|
||||||
|
Region: "spb",
|
||||||
|
Lifetime: 10 * time.Minute,
|
||||||
|
SignTime: time.Now().UTC(),
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := PresignRequestV4a(awsCreds, reqData, presignData)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expBox := &accessbox.Box{
|
||||||
|
Gate: &accessbox.GateData{
|
||||||
|
SecretKey: secretKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mock := newTokensFrostfsMock()
|
||||||
|
mock.addBox(accessKeyAddr, expBox)
|
||||||
|
|
||||||
|
c := &Center{
|
||||||
|
cli: mock,
|
||||||
|
regV4a: NewRegexpMatcher(authorizationFieldV4aRegexp),
|
||||||
|
postReg: NewRegexpMatcher(postPolicyCredentialRegexp),
|
||||||
|
}
|
||||||
|
box, err := c.Authenticate(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.EqualValues(t, expBox, box.AccessBox)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPresignRequestV4a(t *testing.T) {
|
||||||
|
var accessKeyAddr oid.Address
|
||||||
|
err := accessKeyAddr.DecodeString("8N7CYBY74kxZXoyvA5UNdmovaXqFpwNfvEPsqaN81es2/3tDwq5tR8fByrJcyJwyiuYX7Dae8tyDT7pd8oaL1MBto")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
accessKeyID := strings.ReplaceAll(accessKeyAddr.String(), "/", "0")
|
||||||
|
secretKey := "713d0a0b9efc7d22923e17b0402a6a89b4273bc711c8bacb2da1b643d0006aeb"
|
||||||
|
|
||||||
|
signer := v4a.NewSigner(func(options *v4a.SignerOptions) {
|
||||||
|
options.DisableURIPathEscaping = true
|
||||||
|
options.LogSigning = true
|
||||||
|
options.Logger = logging.NewStandardLogger(os.Stdout)
|
||||||
|
})
|
||||||
|
|
||||||
|
credAdapter := v4a.SymmetricCredentialAdaptor{
|
||||||
|
SymmetricProvider: credentialsv2.NewStaticCredentialsProvider(accessKeyID, secretKey, ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
creds, err := credAdapter.RetrievePrivateKey(context.TODO())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
signingTime := time.Now()
|
||||||
|
service := "s3"
|
||||||
|
regionSet := []string{"spb"}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", "http://localhost:8084/bucket/object", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
//req.Header.Set(AmzRegionSet, strings.Join(regionSet, ","))
|
||||||
|
//req.Header.Set(AmzDate, signingTime.Format("20060102T150405Z"))
|
||||||
|
//req.Header.Set(AmzAlgorithm, signaturePreambleSigV4A)
|
||||||
|
|
||||||
|
presignedURL, hdr, err := signer.PresignHTTP(req.Context(), creds, req, "", service, regionSet, signingTime)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
fmt.Println(presignedURL)
|
||||||
|
fmt.Println(hdr)
|
||||||
|
|
||||||
|
signature := req.URL.Query().Get(AmzSignature)
|
||||||
|
|
||||||
|
r, err := http.NewRequest("GET", presignedURL, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = signer.VerifySignature(creds, r, "", service, regionSet, signingTime, signature)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
140
api/auth/signer/v4asdk2/credentials.go
Normal file
140
api/auth/signer/v4asdk2/credentials.go
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
package v4a
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Credentials is Context, ECDSA, and Optional Session Token that can be used
|
||||||
|
// to sign requests using SigV4a
|
||||||
|
type Credentials struct {
|
||||||
|
Context string
|
||||||
|
PrivateKey *ecdsa.PrivateKey
|
||||||
|
SessionToken string
|
||||||
|
|
||||||
|
// Time the credentials will expire.
|
||||||
|
CanExpire bool
|
||||||
|
Expires time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expired returns if the credentials have expired.
|
||||||
|
func (v Credentials) Expired() bool {
|
||||||
|
if v.CanExpire {
|
||||||
|
return !v.Expires.After(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasKeys returns if the credentials keys are set.
|
||||||
|
func (v Credentials) HasKeys() bool {
|
||||||
|
return len(v.Context) > 0 && v.PrivateKey != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SymmetricCredentialAdaptor wraps a SigV4 AccessKey/SecretKey provider and adapts the credentials
|
||||||
|
// to a ECDSA PrivateKey for signing with SiV4a
|
||||||
|
type SymmetricCredentialAdaptor struct {
|
||||||
|
SymmetricProvider aws.CredentialsProvider
|
||||||
|
|
||||||
|
asymmetric atomic.Value
|
||||||
|
m sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve retrieves symmetric credentials from the underlying provider.
|
||||||
|
func (s *SymmetricCredentialAdaptor) Retrieve(ctx context.Context) (aws.Credentials, error) {
|
||||||
|
symCreds, err := s.retrieveFromSymmetricProvider(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return aws.Credentials{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if asymCreds := s.getCreds(); asymCreds == nil {
|
||||||
|
return symCreds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s.m.Lock()
|
||||||
|
defer s.m.Unlock()
|
||||||
|
|
||||||
|
asymCreds := s.getCreds()
|
||||||
|
if asymCreds == nil {
|
||||||
|
return symCreds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the context does not match the access key id clear it
|
||||||
|
if asymCreds.Context != symCreds.AccessKeyID {
|
||||||
|
s.asymmetric.Store((*Credentials)(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
return symCreds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetrievePrivateKey returns credentials suitable for SigV4a signing
|
||||||
|
func (s *SymmetricCredentialAdaptor) RetrievePrivateKey(ctx context.Context) (Credentials, error) {
|
||||||
|
if asymCreds := s.getCreds(); asymCreds != nil {
|
||||||
|
return *asymCreds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s.m.Lock()
|
||||||
|
defer s.m.Unlock()
|
||||||
|
|
||||||
|
if asymCreds := s.getCreds(); asymCreds != nil {
|
||||||
|
return *asymCreds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
symmetricCreds, err := s.retrieveFromSymmetricProvider(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return Credentials{}, fmt.Errorf("failed to retrieve symmetric credentials: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKey, err := deriveKeyFromAccessKeyPair(symmetricCreds.AccessKeyID, symmetricCreds.SecretAccessKey)
|
||||||
|
if err != nil {
|
||||||
|
return Credentials{}, fmt.Errorf("failed to derive assymetric key from credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
creds := Credentials{
|
||||||
|
Context: symmetricCreds.AccessKeyID,
|
||||||
|
PrivateKey: privateKey,
|
||||||
|
SessionToken: symmetricCreds.SessionToken,
|
||||||
|
CanExpire: symmetricCreds.CanExpire,
|
||||||
|
Expires: symmetricCreds.Expires,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.asymmetric.Store(&creds)
|
||||||
|
|
||||||
|
return creds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SymmetricCredentialAdaptor) getCreds() *Credentials {
|
||||||
|
v := s.asymmetric.Load()
|
||||||
|
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c := v.(*Credentials)
|
||||||
|
if c != nil && c.HasKeys() && !c.Expired() {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SymmetricCredentialAdaptor) retrieveFromSymmetricProvider(ctx context.Context) (aws.Credentials, error) {
|
||||||
|
credentials, err := s.SymmetricProvider.Retrieve(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return aws.Credentials{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return credentials, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CredentialsProvider is the interface for a provider to retrieve credentials
|
||||||
|
// to sign requests with.
|
||||||
|
type CredentialsProvider interface {
|
||||||
|
RetrievePrivateKey(context.Context) (Credentials, error)
|
||||||
|
}
|
77
api/auth/signer/v4asdk2/credentials_test.go
Normal file
77
api/auth/signer/v4asdk2/credentials_test.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
package v4a
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rotatingCredsProvider struct {
|
||||||
|
count int
|
||||||
|
fail chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rotatingCredsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) {
|
||||||
|
select {
|
||||||
|
case <-r.fail:
|
||||||
|
return aws.Credentials{}, fmt.Errorf("rotatingCredsProvider error")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
credentials := aws.Credentials{
|
||||||
|
AccessKeyID: fmt.Sprintf("ACCESS_KEY_ID_%d", r.count),
|
||||||
|
SecretAccessKey: fmt.Sprintf("SECRET_ACCESS_KEY_%d", r.count),
|
||||||
|
SessionToken: fmt.Sprintf("SESSION_TOKEN_%d", r.count),
|
||||||
|
}
|
||||||
|
return credentials, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSymmetricCredentialAdaptor(t *testing.T) {
|
||||||
|
provider := &rotatingCredsProvider{
|
||||||
|
count: 0,
|
||||||
|
fail: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
adaptor := &SymmetricCredentialAdaptor{SymmetricProvider: provider}
|
||||||
|
|
||||||
|
if symCreds, err := adaptor.Retrieve(context.Background()); err != nil {
|
||||||
|
t.Fatalf("expect no error, got %v", err)
|
||||||
|
} else if !symCreds.HasKeys() {
|
||||||
|
t.Fatalf("expect symmetric credentials to have keys")
|
||||||
|
}
|
||||||
|
|
||||||
|
if load := adaptor.asymmetric.Load(); load != nil {
|
||||||
|
t.Errorf("expect asymmetric credentials to be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if asymCreds, err := adaptor.RetrievePrivateKey(context.Background()); err != nil {
|
||||||
|
t.Fatalf("expect no error, got %v", err)
|
||||||
|
} else if !asymCreds.HasKeys() {
|
||||||
|
t.Fatalf("expect asymmetric credentials to have keys")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := adaptor.Retrieve(context.Background()); err != nil {
|
||||||
|
t.Fatalf("expect no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if load := adaptor.asymmetric.Load(); load.(*Credentials) == nil {
|
||||||
|
t.Errorf("expect asymmetric credentials to be not nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.count++
|
||||||
|
|
||||||
|
if _, err := adaptor.Retrieve(context.Background()); err != nil {
|
||||||
|
t.Fatalf("expect no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if load := adaptor.asymmetric.Load(); load.(*Credentials) != nil {
|
||||||
|
t.Errorf("expect asymmetric credentials to be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
close(provider.fail) // All requests to the original provider will now fail from this point-on.
|
||||||
|
_, err := adaptor.Retrieve(context.Background())
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expect error, got nil")
|
||||||
|
}
|
||||||
|
}
|
17
api/auth/signer/v4asdk2/error.go
Normal file
17
api/auth/signer/v4asdk2/error.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package v4a
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// SigningError indicates an error condition occurred while performing SigV4a signing
|
||||||
|
type SigningError struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SigningError) Error() string {
|
||||||
|
return fmt.Sprintf("failed to sign request: %v", e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap returns the underlying error cause
|
||||||
|
func (e *SigningError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
30
api/auth/signer/v4asdk2/internal/crypto/compare.go
Normal file
30
api/auth/signer/v4asdk2/internal/crypto/compare.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// ConstantTimeByteCompare is a constant-time byte comparison of x and y. This function performs an absolute comparison
|
||||||
|
// if the two byte slices assuming they represent a big-endian number.
|
||||||
|
//
|
||||||
|
// error if len(x) != len(y)
|
||||||
|
// -1 if x < y
|
||||||
|
// 0 if x == y
|
||||||
|
// +1 if x > y
|
||||||
|
func ConstantTimeByteCompare(x, y []byte) (int, error) {
|
||||||
|
if len(x) != len(y) {
|
||||||
|
return 0, fmt.Errorf("slice lengths do not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
xLarger, yLarger := 0, 0
|
||||||
|
|
||||||
|
for i := 0; i < len(x); i++ {
|
||||||
|
xByte, yByte := int(x[i]), int(y[i])
|
||||||
|
|
||||||
|
x := ((yByte - xByte) >> 8) & 1
|
||||||
|
y := ((xByte - yByte) >> 8) & 1
|
||||||
|
|
||||||
|
xLarger |= x &^ yLarger
|
||||||
|
yLarger |= y &^ xLarger
|
||||||
|
}
|
||||||
|
|
||||||
|
return xLarger - yLarger, nil
|
||||||
|
}
|
60
api/auth/signer/v4asdk2/internal/crypto/compare_test.go
Normal file
60
api/auth/signer/v4asdk2/internal/crypto/compare_test.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"math/big"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConstantTimeByteCompare(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
x, y []byte
|
||||||
|
r int
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{x: []byte{}, y: []byte{}, r: 0},
|
||||||
|
{x: []byte{40}, y: []byte{30}, r: 1},
|
||||||
|
{x: []byte{30}, y: []byte{40}, r: -1},
|
||||||
|
{x: []byte{60, 40, 30, 10, 20}, y: []byte{50, 30, 20, 0, 10}, r: 1},
|
||||||
|
{x: []byte{50, 30, 20, 0, 10}, y: []byte{60, 40, 30, 10, 20}, r: -1},
|
||||||
|
{x: nil, y: []byte{}, r: 0},
|
||||||
|
{x: []byte{}, y: nil, r: 0},
|
||||||
|
{x: []byte{}, y: []byte{10}, expectErr: true},
|
||||||
|
{x: []byte{10}, y: []byte{}, expectErr: true},
|
||||||
|
{x: []byte{10, 20}, y: []byte{10}, expectErr: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range cases {
|
||||||
|
compare, err := ConstantTimeByteCompare(tt.x, tt.y)
|
||||||
|
if (err != nil) != tt.expectErr {
|
||||||
|
t.Fatalf("expectErr=%v, got %v", tt.expectErr, err)
|
||||||
|
}
|
||||||
|
if e, a := tt.r, compare; e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkConstantTimeCompare(b *testing.B) {
|
||||||
|
x, y := big.NewInt(1023), big.NewInt(1024)
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
ConstantTimeByteCompare(x.Bytes(), y.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkCompare(b *testing.B) {
|
||||||
|
x, y := big.NewInt(1023).Bytes(), big.NewInt(1024).Bytes()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
bytes.Compare(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustBigInt(s string) *big.Int {
|
||||||
|
b, ok := (&big.Int{}).SetString(s, 16)
|
||||||
|
if !ok {
|
||||||
|
panic("can't parse as big.Int")
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
113
api/auth/signer/v4asdk2/internal/crypto/ecc.go
Normal file
113
api/auth/signer/v4asdk2/internal/crypto/ecc.go
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/hmac"
|
||||||
|
"encoding/asn1"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
"math"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ecdsaSignature struct {
|
||||||
|
R, S *big.Int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ECDSAKey takes the given elliptic curve, and private key (d) byte slice
|
||||||
|
// and returns the private ECDSA key.
|
||||||
|
func ECDSAKey(curve elliptic.Curve, d []byte) *ecdsa.PrivateKey {
|
||||||
|
return ECDSAKeyFromPoint(curve, (&big.Int{}).SetBytes(d))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ECDSAKeyFromPoint takes the given elliptic curve and point and returns the
|
||||||
|
// private and public keypair
|
||||||
|
func ECDSAKeyFromPoint(curve elliptic.Curve, d *big.Int) *ecdsa.PrivateKey {
|
||||||
|
pX, pY := curve.ScalarBaseMult(d.Bytes())
|
||||||
|
|
||||||
|
privKey := &ecdsa.PrivateKey{
|
||||||
|
PublicKey: ecdsa.PublicKey{
|
||||||
|
Curve: curve,
|
||||||
|
X: pX,
|
||||||
|
Y: pY,
|
||||||
|
},
|
||||||
|
D: d,
|
||||||
|
}
|
||||||
|
|
||||||
|
return privKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// ECDSAPublicKey takes the provide curve and (x, y) coordinates and returns
|
||||||
|
// *ecdsa.PublicKey. Returns an error if the given points are not on the curve.
|
||||||
|
func ECDSAPublicKey(curve elliptic.Curve, x, y []byte) (*ecdsa.PublicKey, error) {
|
||||||
|
xPoint := (&big.Int{}).SetBytes(x)
|
||||||
|
yPoint := (&big.Int{}).SetBytes(y)
|
||||||
|
|
||||||
|
if !curve.IsOnCurve(xPoint, yPoint) {
|
||||||
|
return nil, fmt.Errorf("point(%v, %v) is not on the given curve", xPoint.String(), yPoint.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ecdsa.PublicKey{
|
||||||
|
Curve: curve,
|
||||||
|
X: xPoint,
|
||||||
|
Y: yPoint,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifySignature takes the provided public key, hash, and asn1 encoded signature and returns
|
||||||
|
// whether the given signature is valid.
|
||||||
|
func VerifySignature(key *ecdsa.PublicKey, hash []byte, signature []byte) (bool, error) {
|
||||||
|
var ecdsaSignature ecdsaSignature
|
||||||
|
|
||||||
|
_, err := asn1.Unmarshal(signature, &ecdsaSignature)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ecdsa.Verify(key, hash, ecdsaSignature.R, ecdsaSignature.S), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HMACKeyDerivation provides an implementation of a NIST-800-108 of a KDF (Key Derivation Function) in Counter Mode.
|
||||||
|
// For the purposes of this implantation HMAC is used as the PRF (Pseudorandom function), where the value of
|
||||||
|
// `r` is defined as a 4 byte counter.
|
||||||
|
func HMACKeyDerivation(hash func() hash.Hash, bitLen int, key []byte, label, context []byte) ([]byte, error) {
|
||||||
|
// verify that we won't overflow the counter
|
||||||
|
n := int64(math.Ceil((float64(bitLen) / 8) / float64(hash().Size())))
|
||||||
|
if n > 0x7FFFFFFF {
|
||||||
|
return nil, fmt.Errorf("unable to derive key of size %d using 32-bit counter", bitLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify the requested bit length is not larger then the length encoding size
|
||||||
|
if int64(bitLen) > 0x7FFFFFFF {
|
||||||
|
return nil, fmt.Errorf("bitLen is greater than 32-bits")
|
||||||
|
}
|
||||||
|
|
||||||
|
fixedInput := bytes.NewBuffer(nil)
|
||||||
|
fixedInput.Write(label)
|
||||||
|
fixedInput.WriteByte(0x00)
|
||||||
|
fixedInput.Write(context)
|
||||||
|
if err := binary.Write(fixedInput, binary.BigEndian, int32(bitLen)); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write bit length to fixed input string: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var output []byte
|
||||||
|
|
||||||
|
h := hmac.New(hash, key)
|
||||||
|
|
||||||
|
for i := int64(1); i <= n; i++ {
|
||||||
|
h.Reset()
|
||||||
|
if err := binary.Write(h, binary.BigEndian, int32(i)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err := h.Write(fixedInput.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
output = append(output, h.Sum(nil)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return output[:bitLen/8], nil
|
||||||
|
}
|
277
api/auth/signer/v4asdk2/internal/crypto/ecc_test.go
Normal file
277
api/auth/signer/v4asdk2/internal/crypto/ecc_test.go
Normal file
|
@ -0,0 +1,277 @@
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestECDSAPublicKeyDerivation_P256(t *testing.T) {
|
||||||
|
d := []byte{
|
||||||
|
0xc9, 0x80, 0x68, 0x98, 0xa0, 0x33, 0x49, 0x16, 0xc8, 0x60, 0x74, 0x88, 0x80, 0xa5, 0x41, 0xf0,
|
||||||
|
0x93, 0xb5, 0x79, 0xa9, 0xb1, 0xf3, 0x29, 0x34, 0xd8, 0x6c, 0x36, 0x3c, 0x39, 0x80, 0x03, 0x57,
|
||||||
|
}
|
||||||
|
|
||||||
|
x := []byte{
|
||||||
|
0xd0, 0x72, 0x0d, 0xc6, 0x91, 0xaa, 0x80, 0x09, 0x6b, 0xa3, 0x2f, 0xed, 0x1c, 0xb9, 0x7c, 0x2b,
|
||||||
|
0x62, 0x06, 0x90, 0xd0, 0x6d, 0xe0, 0x31, 0x7b, 0x86, 0x18, 0xd5, 0xce, 0x65, 0xeb, 0x72, 0x8f,
|
||||||
|
}
|
||||||
|
|
||||||
|
y := []byte{
|
||||||
|
0x96, 0x81, 0xb5, 0x17, 0xb1, 0xcd, 0xa1, 0x7d, 0x0d, 0x83, 0xd3, 0x35, 0xd9, 0xc4, 0xa8, 0xa9,
|
||||||
|
0xa9, 0xb0, 0xb1, 0xb3, 0xc7, 0x10, 0x6d, 0x8f, 0x3c, 0x72, 0xbc, 0x50, 0x93, 0xdc, 0x27, 0x5f,
|
||||||
|
}
|
||||||
|
|
||||||
|
testKeyDerivation(t, elliptic.P256(), d, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestECDSAPublicKeyDerivation_P384(t *testing.T) {
|
||||||
|
d := []byte{
|
||||||
|
0x53, 0x94, 0xf7, 0x97, 0x3e, 0xa8, 0x68, 0xc5, 0x2b, 0xf3, 0xff, 0x8d, 0x8c, 0xee, 0xb4, 0xdb,
|
||||||
|
0x90, 0xa6, 0x83, 0x65, 0x3b, 0x12, 0x48, 0x5d, 0x5f, 0x62, 0x7c, 0x3c, 0xe5, 0xab, 0xd8, 0x97,
|
||||||
|
0x8f, 0xc9, 0x67, 0x3d, 0x14, 0xa7, 0x1d, 0x92, 0x57, 0x47, 0x93, 0x16, 0x62, 0x49, 0x3c, 0x37,
|
||||||
|
}
|
||||||
|
|
||||||
|
x := []byte{
|
||||||
|
0xfd, 0x3c, 0x84, 0xe5, 0x68, 0x9b, 0xed, 0x27, 0x0e, 0x60, 0x1b, 0x3d, 0x80, 0xf9, 0x0d, 0x67,
|
||||||
|
0xa9, 0xae, 0x45, 0x1c, 0xce, 0x89, 0x0f, 0x53, 0xe5, 0x83, 0x22, 0x9a, 0xd0, 0xe2, 0xee, 0x64,
|
||||||
|
0x56, 0x11, 0xfa, 0x99, 0x36, 0xdf, 0xa4, 0x53, 0x06, 0xec, 0x18, 0x06, 0x67, 0x74, 0xaa, 0x24,
|
||||||
|
}
|
||||||
|
|
||||||
|
y := []byte{
|
||||||
|
0xb8, 0x3c, 0xa4, 0x12, 0x6c, 0xfc, 0x4c, 0x4d, 0x1d, 0x18, 0xa4, 0xb6, 0xc2, 0x1c, 0x7f, 0x69,
|
||||||
|
0x9d, 0x51, 0x23, 0xdd, 0x9c, 0x24, 0xf6, 0x6f, 0x83, 0x38, 0x46, 0xee, 0xb5, 0x82, 0x96, 0x19,
|
||||||
|
0x6b, 0x42, 0xec, 0x06, 0x42, 0x5d, 0xb5, 0xb7, 0x0a, 0x4b, 0x81, 0xb7, 0xfc, 0xf7, 0x05, 0xa0,
|
||||||
|
}
|
||||||
|
|
||||||
|
testKeyDerivation(t, elliptic.P384(), d, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestECDSAKnownSigningValue_P256(t *testing.T) {
|
||||||
|
d := []byte{
|
||||||
|
0x51, 0x9b, 0x42, 0x3d, 0x71, 0x5f, 0x8b, 0x58, 0x1f, 0x4f, 0xa8, 0xee, 0x59, 0xf4, 0x77, 0x1a,
|
||||||
|
0x5b, 0x44, 0xc8, 0x13, 0x0b, 0x4e, 0x3e, 0xac, 0xca, 0x54, 0xa5, 0x6d, 0xda, 0x72, 0xb4, 0x64,
|
||||||
|
}
|
||||||
|
|
||||||
|
testKnownSigningValue(t, elliptic.P256(), d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestECDSAKnownSigningValue_P384(t *testing.T) {
|
||||||
|
d := []byte{
|
||||||
|
0x53, 0x94, 0xf7, 0x97, 0x3e, 0xa8, 0x68, 0xc5, 0x2b, 0xf3, 0xff, 0x8d, 0x8c, 0xee, 0xb4, 0xdb,
|
||||||
|
0x90, 0xa6, 0x83, 0x65, 0x3b, 0x12, 0x48, 0x5d, 0x5f, 0x62, 0x7c, 0x3c, 0xe5, 0xab, 0xd8, 0x97,
|
||||||
|
0x8f, 0xc9, 0x67, 0x3d, 0x14, 0xa7, 0x1d, 0x92, 0x57, 0x47, 0x93, 0x16, 0x62, 0x49, 0x3c, 0x37,
|
||||||
|
}
|
||||||
|
|
||||||
|
testKnownSigningValue(t, elliptic.P384(), d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testKeyDerivation(t *testing.T, curve elliptic.Curve, d, expectedX, expectedY []byte) {
|
||||||
|
privKey := ECDSAKey(curve, d)
|
||||||
|
|
||||||
|
if e, a := d, privKey.D.Bytes(); bytes.Compare(e, a) != 0 {
|
||||||
|
t.Errorf("expected % x, got % x", e, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, a := expectedX, privKey.X.Bytes(); bytes.Compare(e, a) != 0 {
|
||||||
|
t.Errorf("expected % x, got % x", e, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, a := expectedY, privKey.Y.Bytes(); bytes.Compare(e, a) != 0 {
|
||||||
|
t.Errorf("expected % x, got % x", e, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testKnownSigningValue(t *testing.T, curve elliptic.Curve, d []byte) {
|
||||||
|
signingKey := ECDSAKey(curve, d)
|
||||||
|
|
||||||
|
message := []byte{
|
||||||
|
0x59, 0x05, 0x23, 0x88, 0x77, 0xc7, 0x74, 0x21, 0xf7, 0x3e, 0x43, 0xee, 0x3d, 0xa6, 0xf2, 0xd9,
|
||||||
|
0xe2, 0xcc, 0xad, 0x5f, 0xc9, 0x42, 0xdc, 0xec, 0x0c, 0xbd, 0x25, 0x48, 0x29, 0x35, 0xfa, 0xaf,
|
||||||
|
0x41, 0x69, 0x83, 0xfe, 0x16, 0x5b, 0x1a, 0x04, 0x5e, 0xe2, 0xbc, 0xd2, 0xe6, 0xdc, 0xa3, 0xbd,
|
||||||
|
0xf4, 0x6c, 0x43, 0x10, 0xa7, 0x46, 0x1f, 0x9a, 0x37, 0x96, 0x0c, 0xa6, 0x72, 0xd3, 0xfe, 0xb5,
|
||||||
|
0x47, 0x3e, 0x25, 0x36, 0x05, 0xfb, 0x1d, 0xdf, 0xd2, 0x80, 0x65, 0xb5, 0x3c, 0xb5, 0x85, 0x8a,
|
||||||
|
0x8a, 0xd2, 0x81, 0x75, 0xbf, 0x9b, 0xd3, 0x86, 0xa5, 0xe4, 0x71, 0xea, 0x7a, 0x65, 0xc1, 0x7c,
|
||||||
|
0xc9, 0x34, 0xa9, 0xd7, 0x91, 0xe9, 0x14, 0x91, 0xeb, 0x37, 0x54, 0xd0, 0x37, 0x99, 0x79, 0x0f,
|
||||||
|
0xe2, 0xd3, 0x08, 0xd1, 0x61, 0x46, 0xd5, 0xc9, 0xb0, 0xd0, 0xde, 0xbd, 0x97, 0xd7, 0x9c, 0xe8,
|
||||||
|
}
|
||||||
|
|
||||||
|
sha256Hash := sha256.New()
|
||||||
|
_, err := io.Copy(sha256Hash, bytes.NewReader(message))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msgHash := sha256Hash.Sum(nil)
|
||||||
|
msgSignature, err := signingKey.Sign(rand.Reader, msgHash, crypto.SHA256)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
verified, err := VerifySignature(&signingKey.PublicKey, msgHash, msgSignature)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !verified {
|
||||||
|
t.Fatalf("failed to verify message msgSignature")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestECDSAInvalidSignature_P256(t *testing.T) {
|
||||||
|
testInvalidSignature(t, elliptic.P256())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestECDSAInvalidSignature_P384(t *testing.T) {
|
||||||
|
testInvalidSignature(t, elliptic.P384())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestECDSAGenKeySignature_P256(t *testing.T) {
|
||||||
|
testGenKeySignature(t, elliptic.P256())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestECDSAGenKeySignature_P384(t *testing.T) {
|
||||||
|
testGenKeySignature(t, elliptic.P384())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInvalidSignature(t *testing.T, curve elliptic.Curve) {
|
||||||
|
privateKey, err := ecdsa.GenerateKey(curve, rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to generate key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
message := []byte{
|
||||||
|
0x59, 0x05, 0x23, 0x88, 0x77, 0xc7, 0x74, 0x21, 0xf7, 0x3e, 0x43, 0xee, 0x3d, 0xa6, 0xf2, 0xd9,
|
||||||
|
0xe2, 0xcc, 0xad, 0x5f, 0xc9, 0x42, 0xdc, 0xec, 0x0c, 0xbd, 0x25, 0x48, 0x29, 0x35, 0xfa, 0xaf,
|
||||||
|
0x41, 0x69, 0x83, 0xfe, 0x16, 0x5b, 0x1a, 0x04, 0x5e, 0xe2, 0xbc, 0xd2, 0xe6, 0xdc, 0xa3, 0xbd,
|
||||||
|
0xf4, 0x6c, 0x43, 0x10, 0xa7, 0x46, 0x1f, 0x9a, 0x37, 0x96, 0x0c, 0xa6, 0x72, 0xd3, 0xfe, 0xb5,
|
||||||
|
0x47, 0x3e, 0x25, 0x36, 0x05, 0xfb, 0x1d, 0xdf, 0xd2, 0x80, 0x65, 0xb5, 0x3c, 0xb5, 0x85, 0x8a,
|
||||||
|
0x8a, 0xd2, 0x81, 0x75, 0xbf, 0x9b, 0xd3, 0x86, 0xa5, 0xe4, 0x71, 0xea, 0x7a, 0x65, 0xc1, 0x7c,
|
||||||
|
0xc9, 0x34, 0xa9, 0xd7, 0x91, 0xe9, 0x14, 0x91, 0xeb, 0x37, 0x54, 0xd0, 0x37, 0x99, 0x79, 0x0f,
|
||||||
|
0xe2, 0xd3, 0x08, 0xd1, 0x61, 0x46, 0xd5, 0xc9, 0xb0, 0xd0, 0xde, 0xbd, 0x97, 0xd7, 0x9c, 0xe8,
|
||||||
|
}
|
||||||
|
|
||||||
|
sha256Hash := sha256.New()
|
||||||
|
_, err = io.Copy(sha256Hash, bytes.NewReader(message))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msgHash := sha256Hash.Sum(nil)
|
||||||
|
msgSignature, err := privateKey.Sign(rand.Reader, msgHash, crypto.SHA256)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
byteToFlip := 15
|
||||||
|
switch msgSignature[byteToFlip] {
|
||||||
|
case 0:
|
||||||
|
msgSignature[byteToFlip] = 0x0a
|
||||||
|
default:
|
||||||
|
msgSignature[byteToFlip] &^= msgSignature[byteToFlip]
|
||||||
|
}
|
||||||
|
|
||||||
|
verified, err := VerifySignature(&privateKey.PublicKey, msgHash, msgSignature)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if verified {
|
||||||
|
t.Fatalf("expected message verification to fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGenKeySignature(t *testing.T, curve elliptic.Curve) {
|
||||||
|
privateKey, err := ecdsa.GenerateKey(curve, rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to generate key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
message := []byte{
|
||||||
|
0x59, 0x05, 0x23, 0x88, 0x77, 0xc7, 0x74, 0x21, 0xf7, 0x3e, 0x43, 0xee, 0x3d, 0xa6, 0xf2, 0xd9,
|
||||||
|
0xe2, 0xcc, 0xad, 0x5f, 0xc9, 0x42, 0xdc, 0xec, 0x0c, 0xbd, 0x25, 0x48, 0x29, 0x35, 0xfa, 0xaf,
|
||||||
|
0x41, 0x69, 0x83, 0xfe, 0x16, 0x5b, 0x1a, 0x04, 0x5e, 0xe2, 0xbc, 0xd2, 0xe6, 0xdc, 0xa3, 0xbd,
|
||||||
|
0xf4, 0x6c, 0x43, 0x10, 0xa7, 0x46, 0x1f, 0x9a, 0x37, 0x96, 0x0c, 0xa6, 0x72, 0xd3, 0xfe, 0xb5,
|
||||||
|
0x47, 0x3e, 0x25, 0x36, 0x05, 0xfb, 0x1d, 0xdf, 0xd2, 0x80, 0x65, 0xb5, 0x3c, 0xb5, 0x85, 0x8a,
|
||||||
|
0x8a, 0xd2, 0x81, 0x75, 0xbf, 0x9b, 0xd3, 0x86, 0xa5, 0xe4, 0x71, 0xea, 0x7a, 0x65, 0xc1, 0x7c,
|
||||||
|
0xc9, 0x34, 0xa9, 0xd7, 0x91, 0xe9, 0x14, 0x91, 0xeb, 0x37, 0x54, 0xd0, 0x37, 0x99, 0x79, 0x0f,
|
||||||
|
0xe2, 0xd3, 0x08, 0xd1, 0x61, 0x46, 0xd5, 0xc9, 0xb0, 0xd0, 0xde, 0xbd, 0x97, 0xd7, 0x9c, 0xe8,
|
||||||
|
}
|
||||||
|
|
||||||
|
sha256Hash := sha256.New()
|
||||||
|
_, err = io.Copy(sha256Hash, bytes.NewReader(message))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msgHash := sha256Hash.Sum(nil)
|
||||||
|
msgSignature, err := privateKey.Sign(rand.Reader, msgHash, crypto.SHA256)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
verified, err := VerifySignature(&privateKey.PublicKey, msgHash, msgSignature)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !verified {
|
||||||
|
t.Fatalf("expected message verification to fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestECDSASignatureFormat(t *testing.T) {
|
||||||
|
asn1Signature := []byte{
|
||||||
|
0x30, 0x45, 0x02, 0x21, 0x00, 0xd7, 0xc5, 0xb9, 0x9e, 0x0b, 0xb1, 0x1a, 0x1f, 0x32, 0xda, 0x66, 0xe0, 0xff,
|
||||||
|
0x59, 0xb7, 0x8a, 0x5e, 0xb3, 0x94, 0x9c, 0x23, 0xb3, 0xfc, 0x1f, 0x18, 0xcc, 0xf6, 0x61, 0x67, 0x8b, 0xf1,
|
||||||
|
0xc1, 0x02, 0x20, 0x26, 0x4d, 0x8b, 0x7c, 0xaa, 0x52, 0x4c, 0xc0, 0x2e, 0x5f, 0xf6, 0x7e, 0x24, 0x82, 0xe5,
|
||||||
|
0xfb, 0xcb, 0xc7, 0x9b, 0x83, 0x0d, 0x19, 0x7e, 0x7a, 0x40, 0x37, 0x87, 0xdd, 0x1c, 0x93, 0x13, 0xc4,
|
||||||
|
}
|
||||||
|
|
||||||
|
x := []byte{
|
||||||
|
0x1c, 0xcb, 0xe9, 0x1c, 0x07, 0x5f, 0xc7, 0xf4, 0xf0, 0x33, 0xbf, 0xa2, 0x48, 0xdb, 0x8f, 0xcc,
|
||||||
|
0xd3, 0x56, 0x5d, 0xe9, 0x4b, 0xbf, 0xb1, 0x2f, 0x3c, 0x59, 0xff, 0x46, 0xc2, 0x71, 0xbf, 0x83,
|
||||||
|
}
|
||||||
|
|
||||||
|
y := []byte{
|
||||||
|
0xce, 0x40, 0x14, 0xc6, 0x88, 0x11, 0xf9, 0xa2, 0x1a, 0x1f, 0xdb, 0x2c, 0x0e, 0x61, 0x13, 0xe0,
|
||||||
|
0x6d, 0xb7, 0xca, 0x93, 0xb7, 0x40, 0x4e, 0x78, 0xdc, 0x7c, 0xcd, 0x5c, 0xa8, 0x9a, 0x4c, 0xa9,
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKey, err := ECDSAPublicKey(elliptic.P256(), x, y)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
message := []byte{
|
||||||
|
0x59, 0x05, 0x23, 0x88, 0x77, 0xc7, 0x74, 0x21, 0xf7, 0x3e, 0x43, 0xee, 0x3d, 0xa6, 0xf2, 0xd9,
|
||||||
|
0xe2, 0xcc, 0xad, 0x5f, 0xc9, 0x42, 0xdc, 0xec, 0x0c, 0xbd, 0x25, 0x48, 0x29, 0x35, 0xfa, 0xaf,
|
||||||
|
0x41, 0x69, 0x83, 0xfe, 0x16, 0x5b, 0x1a, 0x04, 0x5e, 0xe2, 0xbc, 0xd2, 0xe6, 0xdc, 0xa3, 0xbd,
|
||||||
|
0xf4, 0x6c, 0x43, 0x10, 0xa7, 0x46, 0x1f, 0x9a, 0x37, 0x96, 0x0c, 0xa6, 0x72, 0xd3, 0xfe, 0xb5,
|
||||||
|
0x47, 0x3e, 0x25, 0x36, 0x05, 0xfb, 0x1d, 0xdf, 0xd2, 0x80, 0x65, 0xb5, 0x3c, 0xb5, 0x85, 0x8a,
|
||||||
|
0x8a, 0xd2, 0x81, 0x75, 0xbf, 0x9b, 0xd3, 0x86, 0xa5, 0xe4, 0x71, 0xea, 0x7a, 0x65, 0xc1, 0x7c,
|
||||||
|
0xc9, 0x34, 0xa9, 0xd7, 0x91, 0xe9, 0x14, 0x91, 0xeb, 0x37, 0x54, 0xd0, 0x37, 0x99, 0x79, 0x0f,
|
||||||
|
0xe2, 0xd3, 0x08, 0xd1, 0x61, 0x46, 0xd5, 0xc9, 0xb0, 0xd0, 0xde, 0xbd, 0x97, 0xd7, 0x9c, 0xe8,
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := sha256.New()
|
||||||
|
_, err = io.Copy(hash, bytes.NewReader(message))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
msgHash := hash.Sum(nil)
|
||||||
|
|
||||||
|
verifySignature, err := VerifySignature(publicKey, msgHash, asn1Signature)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !verifySignature {
|
||||||
|
t.Fatalf("failed to verify signature")
|
||||||
|
}
|
||||||
|
}
|
36
api/auth/signer/v4asdk2/internal/v4/const.go
Normal file
36
api/auth/signer/v4asdk2/internal/v4/const.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
const (
|
||||||
|
// EmptyStringSHA256 is the hex encoded sha256 value of an empty string
|
||||||
|
EmptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
|
||||||
|
|
||||||
|
// UnsignedPayload indicates that the request payload body is unsigned
|
||||||
|
UnsignedPayload = "UNSIGNED-PAYLOAD"
|
||||||
|
|
||||||
|
// AmzAlgorithmKey indicates the signing algorithm
|
||||||
|
AmzAlgorithmKey = "X-Amz-Algorithm"
|
||||||
|
|
||||||
|
// AmzSecurityTokenKey indicates the security token to be used with temporary credentials
|
||||||
|
AmzSecurityTokenKey = "X-Amz-Security-Token"
|
||||||
|
|
||||||
|
// AmzDateKey is the UTC timestamp for the request in the format YYYYMMDD'T'HHMMSS'Z'
|
||||||
|
AmzDateKey = "X-Amz-Date"
|
||||||
|
|
||||||
|
// AmzCredentialKey is the access key ID and credential scope
|
||||||
|
AmzCredentialKey = "X-Amz-Credential"
|
||||||
|
|
||||||
|
// AmzSignedHeadersKey is the set of headers signed for the request
|
||||||
|
AmzSignedHeadersKey = "X-Amz-SignedHeaders"
|
||||||
|
|
||||||
|
// AmzSignatureKey is the query parameter to store the SigV4 signature
|
||||||
|
AmzSignatureKey = "X-Amz-Signature"
|
||||||
|
|
||||||
|
// TimeFormat is the time format to be used in the X-Amz-Date header or query parameter
|
||||||
|
TimeFormat = "20060102T150405Z"
|
||||||
|
|
||||||
|
// ShortTimeFormat is the shorten time format used in the credential scope
|
||||||
|
ShortTimeFormat = "20060102"
|
||||||
|
|
||||||
|
// ContentSHAKey is the SHA256 of request body
|
||||||
|
ContentSHAKey = "X-Amz-Content-Sha256"
|
||||||
|
)
|
88
api/auth/signer/v4asdk2/internal/v4/header_rules.go
Normal file
88
api/auth/signer/v4asdk2/internal/v4/header_rules.go
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rules houses a set of Rule needed for validation of a
|
||||||
|
// string value
|
||||||
|
type Rules []Rule
|
||||||
|
|
||||||
|
// Rule interface allows for more flexible rules and just simply
|
||||||
|
// checks whether or not a value adheres to that Rule
|
||||||
|
type Rule interface {
|
||||||
|
IsValid(value string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid will iterate through all rules and see if any rules
|
||||||
|
// apply to the value and supports nested rules
|
||||||
|
func (r Rules) IsValid(value string) bool {
|
||||||
|
for _, rule := range r {
|
||||||
|
if rule.IsValid(value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapRule generic Rule for maps
|
||||||
|
type MapRule map[string]struct{}
|
||||||
|
|
||||||
|
// IsValid for the map Rule satisfies whether it exists in the map
|
||||||
|
func (m MapRule) IsValid(value string) bool {
|
||||||
|
_, ok := m[value]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowList is a generic Rule for whitelisting
|
||||||
|
type AllowList struct {
|
||||||
|
Rule
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid for AllowList checks if the value is within the AllowList
|
||||||
|
func (w AllowList) IsValid(value string) bool {
|
||||||
|
return w.Rule.IsValid(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DenyList is a generic Rule for blacklisting
|
||||||
|
type DenyList struct {
|
||||||
|
Rule
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid for AllowList checks if the value is within the AllowList
|
||||||
|
func (b DenyList) IsValid(value string) bool {
|
||||||
|
return !b.Rule.IsValid(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patterns is a list of strings to match against
|
||||||
|
type Patterns []string
|
||||||
|
|
||||||
|
// IsValid for Patterns checks each pattern and returns if a match has
|
||||||
|
// been found
|
||||||
|
func (p Patterns) IsValid(value string) bool {
|
||||||
|
for _, pattern := range p {
|
||||||
|
if HasPrefixFold(value, pattern) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// InclusiveRules rules allow for rules to depend on one another
|
||||||
|
type InclusiveRules []Rule
|
||||||
|
|
||||||
|
// IsValid will return true if all rules are true
|
||||||
|
func (r InclusiveRules) IsValid(value string) bool {
|
||||||
|
for _, rule := range r {
|
||||||
|
if !rule.IsValid(value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasPrefixFold tests whether the string s begins with prefix, interpreted as UTF-8 strings,
|
||||||
|
// under Unicode case-folding.
|
||||||
|
func HasPrefixFold(s, prefix string) bool {
|
||||||
|
return len(s) >= len(prefix) && strings.EqualFold(s[0:len(prefix)], prefix)
|
||||||
|
}
|
79
api/auth/signer/v4asdk2/internal/v4/headers.go
Normal file
79
api/auth/signer/v4asdk2/internal/v4/headers.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
// IgnoredPresignedHeaders is a list of headers that are ignored during signing
|
||||||
|
var IgnoredPresignedHeaders = Rules{
|
||||||
|
DenyList{
|
||||||
|
MapRule{
|
||||||
|
"Authorization": struct{}{},
|
||||||
|
"User-Agent": struct{}{},
|
||||||
|
"X-Amzn-Trace-Id": struct{}{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// IgnoredHeaders is a list of headers that are ignored during signing
|
||||||
|
// drop User-Agent header to be compatible with aws sdk java v1.
|
||||||
|
var IgnoredHeaders = Rules{
|
||||||
|
DenyList{
|
||||||
|
MapRule{
|
||||||
|
"Authorization": struct{}{},
|
||||||
|
//"User-Agent": struct{}{},
|
||||||
|
"X-Amzn-Trace-Id": struct{}{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequiredSignedHeaders is a whitelist for Build canonical headers.
|
||||||
|
var RequiredSignedHeaders = Rules{
|
||||||
|
AllowList{
|
||||||
|
MapRule{
|
||||||
|
"Cache-Control": struct{}{},
|
||||||
|
"Content-Disposition": struct{}{},
|
||||||
|
"Content-Encoding": struct{}{},
|
||||||
|
"Content-Language": struct{}{},
|
||||||
|
"Content-Md5": struct{}{},
|
||||||
|
"Content-Type": struct{}{},
|
||||||
|
"Expires": struct{}{},
|
||||||
|
"If-Match": struct{}{},
|
||||||
|
"If-Modified-Since": struct{}{},
|
||||||
|
"If-None-Match": struct{}{},
|
||||||
|
"If-Unmodified-Since": struct{}{},
|
||||||
|
"Range": struct{}{},
|
||||||
|
"X-Amz-Acl": struct{}{},
|
||||||
|
"X-Amz-Copy-Source": struct{}{},
|
||||||
|
"X-Amz-Copy-Source-If-Match": struct{}{},
|
||||||
|
"X-Amz-Copy-Source-If-Modified-Since": struct{}{},
|
||||||
|
"X-Amz-Copy-Source-If-None-Match": struct{}{},
|
||||||
|
"X-Amz-Copy-Source-If-Unmodified-Since": struct{}{},
|
||||||
|
"X-Amz-Copy-Source-Range": struct{}{},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": struct{}{},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": struct{}{},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": struct{}{},
|
||||||
|
"X-Amz-Grant-Full-control": struct{}{},
|
||||||
|
"X-Amz-Grant-Read": struct{}{},
|
||||||
|
"X-Amz-Grant-Read-Acp": struct{}{},
|
||||||
|
"X-Amz-Grant-Write": struct{}{},
|
||||||
|
"X-Amz-Grant-Write-Acp": struct{}{},
|
||||||
|
"X-Amz-Metadata-Directive": struct{}{},
|
||||||
|
"X-Amz-Mfa": struct{}{},
|
||||||
|
"X-Amz-Request-Payer": struct{}{},
|
||||||
|
"X-Amz-Server-Side-Encryption": struct{}{},
|
||||||
|
"X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": struct{}{},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": struct{}{},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key": struct{}{},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": struct{}{},
|
||||||
|
"X-Amz-Storage-Class": struct{}{},
|
||||||
|
"X-Amz-Website-Redirect-Location": struct{}{},
|
||||||
|
"X-Amz-Content-Sha256": struct{}{},
|
||||||
|
"X-Amz-Tagging": struct{}{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Patterns{"X-Amz-Meta-"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowedQueryHoisting is a whitelist for Build query headers. The boolean value
|
||||||
|
// represents whether or not it is a pattern.
|
||||||
|
var AllowedQueryHoisting = InclusiveRules{
|
||||||
|
DenyList{RequiredSignedHeaders},
|
||||||
|
Patterns{"X-Amz-"},
|
||||||
|
}
|
13
api/auth/signer/v4asdk2/internal/v4/hmac.go
Normal file
13
api/auth/signer/v4asdk2/internal/v4/hmac.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HMACSHA256 computes a HMAC-SHA256 of data given the provided key.
|
||||||
|
func HMACSHA256(key []byte, data []byte) []byte {
|
||||||
|
hash := hmac.New(sha256.New, key)
|
||||||
|
hash.Write(data)
|
||||||
|
return hash.Sum(nil)
|
||||||
|
}
|
75
api/auth/signer/v4asdk2/internal/v4/host.go
Normal file
75
api/auth/signer/v4asdk2/internal/v4/host.go
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SanitizeHostForHeader removes default port from host and updates request.Host
|
||||||
|
func SanitizeHostForHeader(r *http.Request) {
|
||||||
|
host := getHost(r)
|
||||||
|
port := portOnly(host)
|
||||||
|
if port != "" && isDefaultPort(r.URL.Scheme, port) {
|
||||||
|
r.Host = stripPort(host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns host from request
|
||||||
|
func getHost(r *http.Request) string {
|
||||||
|
if r.Host != "" {
|
||||||
|
return r.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.URL.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hostname returns u.Host, without any port number.
|
||||||
|
//
|
||||||
|
// If Host is an IPv6 literal with a port number, Hostname returns the
|
||||||
|
// IPv6 literal without the square brackets. IPv6 literals may include
|
||||||
|
// a zone identifier.
|
||||||
|
//
|
||||||
|
// Copied from the Go 1.8 standard library (net/url)
|
||||||
|
func stripPort(hostport string) string {
|
||||||
|
colon := strings.IndexByte(hostport, ':')
|
||||||
|
if colon == -1 {
|
||||||
|
return hostport
|
||||||
|
}
|
||||||
|
if i := strings.IndexByte(hostport, ']'); i != -1 {
|
||||||
|
return strings.TrimPrefix(hostport[:i], "[")
|
||||||
|
}
|
||||||
|
return hostport[:colon]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port returns the port part of u.Host, without the leading colon.
|
||||||
|
// If u.Host doesn't contain a port, Port returns an empty string.
|
||||||
|
//
|
||||||
|
// Copied from the Go 1.8 standard library (net/url)
|
||||||
|
func portOnly(hostport string) string {
|
||||||
|
colon := strings.IndexByte(hostport, ':')
|
||||||
|
if colon == -1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if i := strings.Index(hostport, "]:"); i != -1 {
|
||||||
|
return hostport[i+len("]:"):]
|
||||||
|
}
|
||||||
|
if strings.Contains(hostport, "]") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return hostport[colon+len(":"):]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if the specified URI is using the standard port
|
||||||
|
// (i.e. port 80 for HTTP URIs or 443 for HTTPS URIs)
|
||||||
|
func isDefaultPort(scheme, port string) bool {
|
||||||
|
if port == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
lowerCaseScheme := strings.ToLower(scheme)
|
||||||
|
if (lowerCaseScheme == "http" && port == "80") || (lowerCaseScheme == "https" && port == "443") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
36
api/auth/signer/v4asdk2/internal/v4/time.go
Normal file
36
api/auth/signer/v4asdk2/internal/v4/time.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// SigningTime provides a wrapper around a time.Time which provides cached values for SigV4 signing.
|
||||||
|
type SigningTime struct {
|
||||||
|
time.Time
|
||||||
|
timeFormat string
|
||||||
|
shortTimeFormat string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSigningTime creates a new SigningTime given a time.Time
|
||||||
|
func NewSigningTime(t time.Time) SigningTime {
|
||||||
|
return SigningTime{
|
||||||
|
Time: t,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimeFormat provides a time formatted in the X-Amz-Date format.
|
||||||
|
func (m *SigningTime) TimeFormat() string {
|
||||||
|
return m.format(&m.timeFormat, TimeFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShortTimeFormat provides a time formatted of 20060102.
|
||||||
|
func (m *SigningTime) ShortTimeFormat() string {
|
||||||
|
return m.format(&m.shortTimeFormat, ShortTimeFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SigningTime) format(target *string, format string) string {
|
||||||
|
if len(*target) > 0 {
|
||||||
|
return *target
|
||||||
|
}
|
||||||
|
v := m.Time.Format(format)
|
||||||
|
*target = v
|
||||||
|
return v
|
||||||
|
}
|
64
api/auth/signer/v4asdk2/internal/v4/util.go
Normal file
64
api/auth/signer/v4asdk2/internal/v4/util.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const doubleSpace = " "
|
||||||
|
|
||||||
|
// StripExcessSpaces will rewrite the passed in slice's string values to not
|
||||||
|
// contain muliple side-by-side spaces.
|
||||||
|
func StripExcessSpaces(str string) string {
|
||||||
|
var j, k, l, m, spaces int
|
||||||
|
// Trim trailing spaces
|
||||||
|
for j = len(str) - 1; j >= 0 && str[j] == ' '; j-- {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim leading spaces
|
||||||
|
for k = 0; k < j && str[k] == ' '; k++ {
|
||||||
|
}
|
||||||
|
str = str[k : j+1]
|
||||||
|
|
||||||
|
// Strip multiple spaces.
|
||||||
|
j = strings.Index(str, doubleSpace)
|
||||||
|
if j < 0 {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := []byte(str)
|
||||||
|
for k, m, l = j, j, len(buf); k < l; k++ {
|
||||||
|
if buf[k] == ' ' {
|
||||||
|
if spaces == 0 {
|
||||||
|
// First space.
|
||||||
|
buf[m] = buf[k]
|
||||||
|
m++
|
||||||
|
}
|
||||||
|
spaces++
|
||||||
|
} else {
|
||||||
|
// End of multiple spaces.
|
||||||
|
spaces = 0
|
||||||
|
buf[m] = buf[k]
|
||||||
|
m++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(buf[:m])
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetURIPath returns the escaped URI component from the provided URL
|
||||||
|
func GetURIPath(u *url.URL) string {
|
||||||
|
var uri string
|
||||||
|
|
||||||
|
if len(u.Opaque) > 0 {
|
||||||
|
uri = "/" + strings.Join(strings.Split(u.Opaque, "/")[3:], "/")
|
||||||
|
} else {
|
||||||
|
uri = u.EscapedPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(uri) == 0 {
|
||||||
|
uri = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
return uri
|
||||||
|
}
|
75
api/auth/signer/v4asdk2/internal/v4/util_test.go
Normal file
75
api/auth/signer/v4asdk2/internal/v4/util_test.go
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStripExcessHeaders(t *testing.T) {
|
||||||
|
vals := []string{
|
||||||
|
"",
|
||||||
|
"123",
|
||||||
|
"1 2 3",
|
||||||
|
"1 2 3 ",
|
||||||
|
" 1 2 3",
|
||||||
|
"1 2 3",
|
||||||
|
"1 23",
|
||||||
|
"1 2 3",
|
||||||
|
"1 2 ",
|
||||||
|
" 1 2 ",
|
||||||
|
"12 3",
|
||||||
|
"12 3 1",
|
||||||
|
"12 3 1",
|
||||||
|
"12 3 1abc123",
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []string{
|
||||||
|
"",
|
||||||
|
"123",
|
||||||
|
"1 2 3",
|
||||||
|
"1 2 3",
|
||||||
|
"1 2 3",
|
||||||
|
"1 2 3",
|
||||||
|
"1 23",
|
||||||
|
"1 2 3",
|
||||||
|
"1 2",
|
||||||
|
"1 2",
|
||||||
|
"12 3",
|
||||||
|
"12 3 1",
|
||||||
|
"12 3 1",
|
||||||
|
"12 3 1abc123",
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(vals); i++ {
|
||||||
|
r := StripExcessSpaces(vals[i])
|
||||||
|
if e, a := expected[i], r; e != a {
|
||||||
|
t.Errorf("%d, expect %v, got %v", i, e, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var stripExcessSpaceCases = []string{
|
||||||
|
`AWS4-HMAC-SHA256 Credential=AKIDFAKEIDFAKEID/20160628/us-west-2/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=1234567890abcdef1234567890abcdef1234567890abcdef`,
|
||||||
|
`123 321 123 321`,
|
||||||
|
` 123 321 123 321 `,
|
||||||
|
` 123 321 123 321 `,
|
||||||
|
"123",
|
||||||
|
"1 2 3",
|
||||||
|
" 1 2 3",
|
||||||
|
"1 2 3",
|
||||||
|
"1 23",
|
||||||
|
"1 2 3",
|
||||||
|
"1 2 ",
|
||||||
|
" 1 2 ",
|
||||||
|
"12 3",
|
||||||
|
"12 3 1",
|
||||||
|
"12 3 1",
|
||||||
|
"12 3 1abc123",
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkStripExcessSpaces(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
for _, v := range stripExcessSpaceCases {
|
||||||
|
StripExcessSpaces(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
118
api/auth/signer/v4asdk2/middleware.go
Normal file
118
api/auth/signer/v4asdk2/middleware.go
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
package v4a
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
||||||
|
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
||||||
|
"github.com/aws/smithy-go/middleware"
|
||||||
|
smithyhttp "github.com/aws/smithy-go/transport/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPSigner is SigV4a HTTP signer implementation
|
||||||
|
type HTTPSigner interface {
|
||||||
|
SignHTTP(ctx context.Context, credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, optfns ...func(*SignerOptions)) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignHTTPRequestMiddlewareOptions is the middleware options for constructing a SignHTTPRequestMiddleware.
|
||||||
|
type SignHTTPRequestMiddlewareOptions struct {
|
||||||
|
Credentials CredentialsProvider
|
||||||
|
Signer HTTPSigner
|
||||||
|
LogSigning bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignHTTPRequestMiddleware is a middleware for signing an HTTP request using SigV4a.
|
||||||
|
type SignHTTPRequestMiddleware struct {
|
||||||
|
credentials CredentialsProvider
|
||||||
|
signer HTTPSigner
|
||||||
|
logSigning bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSignHTTPRequestMiddleware constructs a SignHTTPRequestMiddleware using the given SignHTTPRequestMiddlewareOptions.
|
||||||
|
func NewSignHTTPRequestMiddleware(options SignHTTPRequestMiddlewareOptions) *SignHTTPRequestMiddleware {
|
||||||
|
return &SignHTTPRequestMiddleware{
|
||||||
|
credentials: options.Credentials,
|
||||||
|
signer: options.Signer,
|
||||||
|
logSigning: options.LogSigning,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID the middleware identifier.
|
||||||
|
func (s *SignHTTPRequestMiddleware) ID() string {
|
||||||
|
return "Signing"
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleFinalize signs an HTTP request using SigV4a.
|
||||||
|
func (s *SignHTTPRequestMiddleware) HandleFinalize(
|
||||||
|
ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler,
|
||||||
|
) (
|
||||||
|
out middleware.FinalizeOutput, metadata middleware.Metadata, err error,
|
||||||
|
) {
|
||||||
|
if !hasCredentialProvider(s.credentials) {
|
||||||
|
return next.HandleFinalize(ctx, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, ok := in.Request.(*smithyhttp.Request)
|
||||||
|
if !ok {
|
||||||
|
return out, metadata, fmt.Errorf("unexpected request middleware type %T", in.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
signingName, signingRegion := awsmiddleware.GetSigningName(ctx), awsmiddleware.GetSigningRegion(ctx)
|
||||||
|
payloadHash := v4.GetPayloadHash(ctx)
|
||||||
|
if len(payloadHash) == 0 {
|
||||||
|
return out, metadata, &SigningError{Err: fmt.Errorf("computed payload hash missing from context")}
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials, err := s.credentials.RetrievePrivateKey(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return out, metadata, &SigningError{Err: fmt.Errorf("failed to retrieve credentials: %w", err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
signerOptions := []func(o *SignerOptions){
|
||||||
|
func(o *SignerOptions) {
|
||||||
|
o.Logger = middleware.GetLogger(ctx)
|
||||||
|
o.LogSigning = s.logSigning
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// existing DisableURIPathEscaping is equivalent in purpose
|
||||||
|
// to authentication scheme property DisableDoubleEncoding
|
||||||
|
//disableDoubleEncoding, overridden := internalauth.GetDisableDoubleEncoding(ctx) // internalauth "github.com/aws/aws-sdk-go-v2/internal/auth"
|
||||||
|
//if overridden {
|
||||||
|
// signerOptions = append(signerOptions, func(o *SignerOptions) {
|
||||||
|
// o.DisableURIPathEscaping = disableDoubleEncoding
|
||||||
|
// })
|
||||||
|
//}
|
||||||
|
|
||||||
|
err = s.signer.SignHTTP(ctx, credentials, req.Request, payloadHash, signingName, []string{signingRegion}, time.Now().UTC(), signerOptions...)
|
||||||
|
if err != nil {
|
||||||
|
return out, metadata, &SigningError{Err: fmt.Errorf("failed to sign http request, %w", err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next.HandleFinalize(ctx, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasCredentialProvider(p CredentialsProvider) bool {
|
||||||
|
if p == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterSigningMiddleware registers the SigV4a signing middleware to the stack. If a signing middleware is already
|
||||||
|
// present, this provided middleware will be swapped. Otherwise the middleware will be added at the tail of the
|
||||||
|
// finalize step.
|
||||||
|
func RegisterSigningMiddleware(stack *middleware.Stack, signingMiddleware *SignHTTPRequestMiddleware) (err error) {
|
||||||
|
const signedID = "Signing"
|
||||||
|
_, present := stack.Finalize.Get(signedID)
|
||||||
|
if present {
|
||||||
|
_, err = stack.Finalize.Swap(signedID, signingMiddleware)
|
||||||
|
} else {
|
||||||
|
err = stack.Finalize.Add(signingMiddleware, middleware.After)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
149
api/auth/signer/v4asdk2/middleware_test.go
Normal file
149
api/auth/signer/v4asdk2/middleware_test.go
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
package v4a
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
||||||
|
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
||||||
|
"github.com/aws/smithy-go/logging"
|
||||||
|
"github.com/aws/smithy-go/middleware"
|
||||||
|
smithyhttp "github.com/aws/smithy-go/transport/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stubCredentialsProviderFunc func(context.Context) (Credentials, error)
|
||||||
|
|
||||||
|
func (f stubCredentialsProviderFunc) RetrievePrivateKey(ctx context.Context) (Credentials, error) {
|
||||||
|
return f(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpSignerFunc func(ctx context.Context, credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, optFns ...func(*SignerOptions)) error
|
||||||
|
|
||||||
|
func (f httpSignerFunc) SignHTTP(ctx context.Context, credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, optFns ...func(*SignerOptions)) error {
|
||||||
|
return f(ctx, credentials, r, payloadHash, service, regionSet, signingTime, optFns...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignHTTPRequestMiddleware(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
creds CredentialsProvider
|
||||||
|
hash string
|
||||||
|
logSigning bool
|
||||||
|
expectedErr interface{}
|
||||||
|
}{
|
||||||
|
"success": {
|
||||||
|
creds: stubCredentials,
|
||||||
|
hash: "0123456789abcdef",
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
creds: stubCredentialsProviderFunc(func(ctx context.Context) (Credentials, error) {
|
||||||
|
return Credentials{}, fmt.Errorf("credential error")
|
||||||
|
}),
|
||||||
|
hash: "",
|
||||||
|
expectedErr: &SigningError{},
|
||||||
|
},
|
||||||
|
"nil creds": {
|
||||||
|
creds: nil,
|
||||||
|
},
|
||||||
|
"with log signing": {
|
||||||
|
creds: stubCredentials,
|
||||||
|
hash: "0123456789abcdef",
|
||||||
|
logSigning: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
signingName = "serviceId"
|
||||||
|
signingRegion = "regionName"
|
||||||
|
)
|
||||||
|
|
||||||
|
for name, tt := range cases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
c := &SignHTTPRequestMiddleware{
|
||||||
|
credentials: tt.creds,
|
||||||
|
signer: httpSignerFunc(
|
||||||
|
func(ctx context.Context,
|
||||||
|
credentials Credentials, r *http.Request, payloadHash string,
|
||||||
|
service string, regionSet []string, signingTime time.Time,
|
||||||
|
optFns ...func(*SignerOptions),
|
||||||
|
) error {
|
||||||
|
var options SignerOptions
|
||||||
|
for _, fn := range optFns {
|
||||||
|
fn(&options)
|
||||||
|
}
|
||||||
|
if options.Logger == nil {
|
||||||
|
t.Errorf("expect logger, got none")
|
||||||
|
}
|
||||||
|
if options.LogSigning {
|
||||||
|
options.Logger.Logf(logging.Debug, t.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
expectCreds, _ := tt.creds.RetrievePrivateKey(ctx)
|
||||||
|
if diff := cmpDiff(expectCreds, credentials); len(diff) > 0 {
|
||||||
|
t.Error(diff)
|
||||||
|
}
|
||||||
|
if e, a := tt.hash, payloadHash; e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := signingName, service; e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if diff := cmpDiff([]string{signingRegion}, regionSet); len(diff) > 0 {
|
||||||
|
t.Error(diff)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
logSigning: tt.logSigning,
|
||||||
|
}
|
||||||
|
|
||||||
|
next := middleware.FinalizeHandlerFunc(func(ctx context.Context, in middleware.FinalizeInput) (out middleware.FinalizeOutput, metadata middleware.Metadata, err error) {
|
||||||
|
return out, metadata, err
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := awsmiddleware.SetSigningRegion(
|
||||||
|
awsmiddleware.SetSigningName(context.Background(), signingName),
|
||||||
|
signingRegion)
|
||||||
|
|
||||||
|
var loggerBuf bytes.Buffer
|
||||||
|
logger := logging.NewStandardLogger(&loggerBuf)
|
||||||
|
ctx = middleware.SetLogger(ctx, logger)
|
||||||
|
|
||||||
|
if len(tt.hash) != 0 {
|
||||||
|
ctx = v4.SetPayloadHash(ctx, tt.hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err := c.HandleFinalize(ctx, middleware.FinalizeInput{
|
||||||
|
Request: &smithyhttp.Request{Request: &http.Request{}},
|
||||||
|
}, next)
|
||||||
|
if err != nil && tt.expectedErr == nil {
|
||||||
|
t.Errorf("expected no error, got %v", err)
|
||||||
|
} else if err != nil && tt.expectedErr != nil {
|
||||||
|
e, a := tt.expectedErr, err
|
||||||
|
if !errors.As(a, &e) {
|
||||||
|
t.Errorf("expected error type %T, got %T", e, a)
|
||||||
|
}
|
||||||
|
} else if err == nil && tt.expectedErr != nil {
|
||||||
|
t.Errorf("expected error, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.logSigning {
|
||||||
|
if e, a := t.Name(), loggerBuf.String(); !strings.Contains(a, e) {
|
||||||
|
t.Errorf("expect %v logged in %v", e, a)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if loggerBuf.Len() != 0 {
|
||||||
|
t.Errorf("expect no log, got %v", loggerBuf.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ middleware.FinalizeMiddleware = &SignHTTPRequestMiddleware{}
|
||||||
|
)
|
116
api/auth/signer/v4asdk2/presign_middleware.go
Normal file
116
api/auth/signer/v4asdk2/presign_middleware.go
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
package v4a
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
||||||
|
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
||||||
|
"github.com/aws/smithy-go/middleware"
|
||||||
|
smithyHTTP "github.com/aws/smithy-go/transport/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPPresigner is an interface to a SigV4a signer that can sign create a
|
||||||
|
// presigned URL for a HTTP requests.
|
||||||
|
type HTTPPresigner interface {
|
||||||
|
PresignHTTP(
|
||||||
|
ctx context.Context, credentials Credentials, r *http.Request,
|
||||||
|
payloadHash string, service string, regionSet []string, signingTime time.Time,
|
||||||
|
optFns ...func(*SignerOptions),
|
||||||
|
) (url string, signedHeader http.Header, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PresignHTTPRequestMiddlewareOptions is the options for the PresignHTTPRequestMiddleware middleware.
|
||||||
|
type PresignHTTPRequestMiddlewareOptions struct {
|
||||||
|
CredentialsProvider CredentialsProvider
|
||||||
|
Presigner HTTPPresigner
|
||||||
|
LogSigning bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// PresignHTTPRequestMiddleware provides the Finalize middleware for creating a
|
||||||
|
// presigned URL for an HTTP request.
|
||||||
|
//
|
||||||
|
// Will short circuit the middleware stack and not forward onto the next
|
||||||
|
// Finalize handler.
|
||||||
|
type PresignHTTPRequestMiddleware struct {
|
||||||
|
credentialsProvider CredentialsProvider
|
||||||
|
presigner HTTPPresigner
|
||||||
|
logSigning bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPresignHTTPRequestMiddleware returns a new PresignHTTPRequestMiddleware
|
||||||
|
// initialized with the presigner.
|
||||||
|
func NewPresignHTTPRequestMiddleware(options PresignHTTPRequestMiddlewareOptions) *PresignHTTPRequestMiddleware {
|
||||||
|
return &PresignHTTPRequestMiddleware{
|
||||||
|
credentialsProvider: options.CredentialsProvider,
|
||||||
|
presigner: options.Presigner,
|
||||||
|
logSigning: options.LogSigning,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID provides the middleware ID.
|
||||||
|
func (*PresignHTTPRequestMiddleware) ID() string { return "PresignHTTPRequest" }
|
||||||
|
|
||||||
|
// HandleFinalize will take the provided input and create a presigned url for
|
||||||
|
// the http request using the SigV4 presign authentication scheme.
|
||||||
|
func (s *PresignHTTPRequestMiddleware) HandleFinalize(
|
||||||
|
ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler,
|
||||||
|
) (
|
||||||
|
out middleware.FinalizeOutput, metadata middleware.Metadata, err error,
|
||||||
|
) {
|
||||||
|
req, ok := in.Request.(*smithyHTTP.Request)
|
||||||
|
if !ok {
|
||||||
|
return out, metadata, &SigningError{
|
||||||
|
Err: fmt.Errorf("unexpected request middleware type %T", in.Request),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReq := req.Build(ctx)
|
||||||
|
if !hasCredentialProvider(s.credentialsProvider) {
|
||||||
|
out.Result = &v4.PresignedHTTPRequest{
|
||||||
|
URL: httpReq.URL.String(),
|
||||||
|
Method: httpReq.Method,
|
||||||
|
SignedHeader: http.Header{},
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, metadata, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
signingName := awsmiddleware.GetSigningName(ctx)
|
||||||
|
signingRegion := awsmiddleware.GetSigningRegion(ctx)
|
||||||
|
payloadHash := v4.GetPayloadHash(ctx)
|
||||||
|
if len(payloadHash) == 0 {
|
||||||
|
return out, metadata, &SigningError{
|
||||||
|
Err: fmt.Errorf("computed payload hash missing from context"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials, err := s.credentialsProvider.RetrievePrivateKey(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return out, metadata, &SigningError{
|
||||||
|
Err: fmt.Errorf("failed to retrieve credentials: %w", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
u, h, err := s.presigner.PresignHTTP(ctx, credentials,
|
||||||
|
httpReq, payloadHash, signingName, []string{signingRegion}, time.Now(),
|
||||||
|
func(o *SignerOptions) {
|
||||||
|
o.Logger = middleware.GetLogger(ctx)
|
||||||
|
o.LogSigning = s.logSigning
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return out, metadata, &SigningError{
|
||||||
|
Err: fmt.Errorf("failed to sign http request, %w", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.Result = &v4.PresignedHTTPRequest{
|
||||||
|
URL: u,
|
||||||
|
Method: httpReq.Method,
|
||||||
|
SignedHeader: h,
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, metadata, nil
|
||||||
|
}
|
222
api/auth/signer/v4asdk2/presign_middleware_test.go
Normal file
222
api/auth/signer/v4asdk2/presign_middleware_test.go
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
package v4a
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
||||||
|
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
||||||
|
"github.com/aws/smithy-go/logging"
|
||||||
|
"github.com/aws/smithy-go/middleware"
|
||||||
|
smithyhttp "github.com/aws/smithy-go/transport/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type httpPresignerFunc func(
|
||||||
|
ctx context.Context, credentials Credentials, r *http.Request,
|
||||||
|
payloadHash string, service string, regionSet []string, signingTime time.Time,
|
||||||
|
optFns ...func(*SignerOptions),
|
||||||
|
) (url string, signedHeader http.Header, err error)
|
||||||
|
|
||||||
|
func (f httpPresignerFunc) PresignHTTP(
|
||||||
|
ctx context.Context, credentials Credentials, r *http.Request,
|
||||||
|
payloadHash string, service string, regionSet []string, signingTime time.Time,
|
||||||
|
optFns ...func(*SignerOptions),
|
||||||
|
) (
|
||||||
|
url string, signedHeader http.Header, err error,
|
||||||
|
) {
|
||||||
|
return f(ctx, credentials, r, payloadHash, service, regionSet, signingTime, optFns...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPresignHTTPRequestMiddleware(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
Request *http.Request
|
||||||
|
Creds CredentialsProvider
|
||||||
|
PayloadHash string
|
||||||
|
LogSigning bool
|
||||||
|
ExpectResult *v4.PresignedHTTPRequest
|
||||||
|
ExpectErr string
|
||||||
|
}{
|
||||||
|
"success": {
|
||||||
|
Request: &http.Request{
|
||||||
|
URL: func() *url.URL {
|
||||||
|
u, _ := url.Parse("https://example.aws/path?query=foo")
|
||||||
|
return u
|
||||||
|
}(),
|
||||||
|
Header: http.Header{},
|
||||||
|
},
|
||||||
|
Creds: stubCredentials,
|
||||||
|
PayloadHash: "0123456789abcdef",
|
||||||
|
ExpectResult: &v4.PresignedHTTPRequest{
|
||||||
|
URL: "https://example.aws/path?query=foo",
|
||||||
|
SignedHeader: http.Header{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
Request: func() *http.Request {
|
||||||
|
return &http.Request{}
|
||||||
|
}(),
|
||||||
|
Creds: stubCredentials,
|
||||||
|
PayloadHash: "",
|
||||||
|
ExpectErr: "failed to sign request",
|
||||||
|
},
|
||||||
|
"anonymous creds": {
|
||||||
|
Request: &http.Request{
|
||||||
|
URL: func() *url.URL {
|
||||||
|
u, _ := url.Parse("https://example.aws/path?query=foo")
|
||||||
|
return u
|
||||||
|
}(),
|
||||||
|
Header: http.Header{},
|
||||||
|
},
|
||||||
|
Creds: stubCredentials,
|
||||||
|
PayloadHash: "",
|
||||||
|
ExpectErr: "failed to sign request",
|
||||||
|
ExpectResult: &v4.PresignedHTTPRequest{
|
||||||
|
URL: "https://example.aws/path?query=foo",
|
||||||
|
SignedHeader: http.Header{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"nil creds": {
|
||||||
|
Request: &http.Request{
|
||||||
|
URL: func() *url.URL {
|
||||||
|
u, _ := url.Parse("https://example.aws/path?query=foo")
|
||||||
|
return u
|
||||||
|
}(),
|
||||||
|
Header: http.Header{},
|
||||||
|
},
|
||||||
|
Creds: nil,
|
||||||
|
ExpectResult: &v4.PresignedHTTPRequest{
|
||||||
|
URL: "https://example.aws/path?query=foo",
|
||||||
|
SignedHeader: http.Header{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"with log signing": {
|
||||||
|
Request: &http.Request{
|
||||||
|
URL: func() *url.URL {
|
||||||
|
u, _ := url.Parse("https://example.aws/path?query=foo")
|
||||||
|
return u
|
||||||
|
}(),
|
||||||
|
Header: http.Header{},
|
||||||
|
},
|
||||||
|
Creds: stubCredentials,
|
||||||
|
PayloadHash: "0123456789abcdef",
|
||||||
|
ExpectResult: &v4.PresignedHTTPRequest{
|
||||||
|
URL: "https://example.aws/path?query=foo",
|
||||||
|
SignedHeader: http.Header{},
|
||||||
|
},
|
||||||
|
|
||||||
|
LogSigning: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
signingName = "serviceId"
|
||||||
|
signingRegion = "regionName"
|
||||||
|
)
|
||||||
|
|
||||||
|
for name, tt := range cases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
m := &PresignHTTPRequestMiddleware{
|
||||||
|
credentialsProvider: tt.Creds,
|
||||||
|
|
||||||
|
presigner: httpPresignerFunc(func(
|
||||||
|
ctx context.Context, credentials Credentials, r *http.Request,
|
||||||
|
payloadHash string, service string, regionSet []string, signingTime time.Time,
|
||||||
|
optFns ...func(*SignerOptions),
|
||||||
|
) (url string, signedHeader http.Header, err error) {
|
||||||
|
var options SignerOptions
|
||||||
|
for _, fn := range optFns {
|
||||||
|
fn(&options)
|
||||||
|
}
|
||||||
|
if options.Logger == nil {
|
||||||
|
t.Errorf("expect logger, got none")
|
||||||
|
}
|
||||||
|
if options.LogSigning {
|
||||||
|
options.Logger.Logf(logging.Debug, t.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasCredentialProvider(tt.Creds) {
|
||||||
|
t.Errorf("expect presigner not to be called for not credentials provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectCreds, _ := tt.Creds.RetrievePrivateKey(context.Background())
|
||||||
|
if diff := cmpDiff(expectCreds, credentials); len(diff) > 0 {
|
||||||
|
t.Error(diff)
|
||||||
|
}
|
||||||
|
if e, a := tt.PayloadHash, payloadHash; e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := signingName, service; e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if diff := cmpDiff([]string{signingRegion}, regionSet); len(diff) > 0 {
|
||||||
|
t.Error(diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tt.ExpectResult.URL, tt.ExpectResult.SignedHeader, nil
|
||||||
|
}),
|
||||||
|
logSigning: tt.LogSigning,
|
||||||
|
}
|
||||||
|
|
||||||
|
next := middleware.FinalizeHandlerFunc(
|
||||||
|
func(ctx context.Context, in middleware.FinalizeInput) (
|
||||||
|
out middleware.FinalizeOutput, metadata middleware.Metadata, err error,
|
||||||
|
) {
|
||||||
|
t.Errorf("expect next handler not to be called")
|
||||||
|
return out, metadata, err
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := awsmiddleware.SetSigningRegion(
|
||||||
|
awsmiddleware.SetSigningName(context.Background(), signingName),
|
||||||
|
signingRegion)
|
||||||
|
|
||||||
|
var loggerBuf bytes.Buffer
|
||||||
|
logger := logging.NewStandardLogger(&loggerBuf)
|
||||||
|
ctx = middleware.SetLogger(ctx, logger)
|
||||||
|
|
||||||
|
if len(tt.PayloadHash) != 0 {
|
||||||
|
ctx = v4.SetPayloadHash(ctx, tt.PayloadHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, _, err := m.HandleFinalize(ctx, middleware.FinalizeInput{
|
||||||
|
Request: &smithyhttp.Request{
|
||||||
|
Request: tt.Request,
|
||||||
|
},
|
||||||
|
}, next)
|
||||||
|
if len(tt.ExpectErr) != 0 {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expect error, got none")
|
||||||
|
}
|
||||||
|
if e, a := tt.ExpectErr, err.Error(); !strings.Contains(a, e) {
|
||||||
|
t.Fatalf("expect error to contain %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expect no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmpDiff(tt.ExpectResult, result.Result); len(diff) != 0 {
|
||||||
|
t.Errorf("expect result match\n%v", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.LogSigning {
|
||||||
|
if e, a := t.Name(), loggerBuf.String(); !strings.Contains(a, e) {
|
||||||
|
t.Errorf("expect %v logged in %v", e, a)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if loggerBuf.Len() != 0 {
|
||||||
|
t.Errorf("expect no log, got %v", loggerBuf.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ middleware.FinalizeMiddleware = &PresignHTTPRequestMiddleware{}
|
||||||
|
)
|
18
api/auth/signer/v4asdk2/shared_test.go
Normal file
18
api/auth/signer/v4asdk2/shared_test.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package v4a
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
)
|
||||||
|
|
||||||
|
var stubCredentials = stubCredentialsProviderFunc(func(ctx context.Context) (Credentials, error) {
|
||||||
|
stubKey, err := ecdsa.GenerateKey(p256, bytes.NewReader(bytes.Repeat([]byte{1}, 40)))
|
||||||
|
if err != nil {
|
||||||
|
return Credentials{}, err
|
||||||
|
}
|
||||||
|
return Credentials{
|
||||||
|
Context: "STUB",
|
||||||
|
PrivateKey: stubKey,
|
||||||
|
}, nil
|
||||||
|
})
|
85
api/auth/signer/v4asdk2/smithy.go
Normal file
85
api/auth/signer/v4asdk2/smithy.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
package v4a
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
||||||
|
"github.com/aws/smithy-go"
|
||||||
|
"github.com/aws/smithy-go/auth"
|
||||||
|
"github.com/aws/smithy-go/logging"
|
||||||
|
smithyhttp "github.com/aws/smithy-go/transport/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CredentialsAdapter adapts v4a.Credentials to smithy auth.Identity.
|
||||||
|
type CredentialsAdapter struct {
|
||||||
|
Credentials Credentials
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ auth.Identity = (*CredentialsAdapter)(nil)
|
||||||
|
|
||||||
|
// Expiration returns the time of expiration for the credentials.
|
||||||
|
func (v *CredentialsAdapter) Expiration() time.Time {
|
||||||
|
return v.Credentials.Expires
|
||||||
|
}
|
||||||
|
|
||||||
|
// CredentialsProviderAdapter adapts v4a.CredentialsProvider to
|
||||||
|
// auth.IdentityResolver.
|
||||||
|
type CredentialsProviderAdapter struct {
|
||||||
|
Provider CredentialsProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ (auth.IdentityResolver) = (*CredentialsProviderAdapter)(nil)
|
||||||
|
|
||||||
|
// GetIdentity retrieves v4a credentials using the underlying provider.
|
||||||
|
func (v *CredentialsProviderAdapter) GetIdentity(ctx context.Context, _ smithy.Properties) (
|
||||||
|
auth.Identity, error,
|
||||||
|
) {
|
||||||
|
creds, err := v.Provider.RetrievePrivateKey(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get credentials: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CredentialsAdapter{Credentials: creds}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignerAdapter adapts v4a.HTTPSigner to smithy http.Signer.
|
||||||
|
type SignerAdapter struct {
|
||||||
|
Signer HTTPSigner
|
||||||
|
Logger logging.Logger
|
||||||
|
LogSigning bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ (smithyhttp.Signer) = (*SignerAdapter)(nil)
|
||||||
|
|
||||||
|
// SignRequest signs the request with the provided identity.
|
||||||
|
func (v *SignerAdapter) SignRequest(ctx context.Context, r *smithyhttp.Request, identity auth.Identity, props smithy.Properties) error {
|
||||||
|
ca, ok := identity.(*CredentialsAdapter)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unexpected identity type: %T", identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
name, ok := smithyhttp.GetSigV4SigningName(&props)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("sigv4a signing name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
regions, ok := smithyhttp.GetSigV4ASigningRegions(&props)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("sigv4a signing region is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := v4.GetPayloadHash(ctx)
|
||||||
|
err := v.Signer.SignHTTP(ctx, ca.Credentials, r.Request, hash, name, regions, time.Now(), func(o *SignerOptions) {
|
||||||
|
o.DisableURIPathEscaping, _ = smithyhttp.GetDisableDoubleEncoding(&props)
|
||||||
|
|
||||||
|
o.Logger = v.Logger
|
||||||
|
o.LogSigning = v.LogSigning
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("sign http: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
96
api/auth/signer/v4asdk2/stream.go
Normal file
96
api/auth/signer/v4asdk2/stream.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package v4a
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
signerCrypto "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2/internal/crypto"
|
||||||
|
v4Internal "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2/internal/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EventStreamSigner is an AWS EventStream protocol signer.
|
||||||
|
type EventStreamSigner interface {
|
||||||
|
GetSignature(ctx context.Context, headers, payload []byte, signingTime time.Time, optFns ...func(*StreamSignerOptions)) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamSignerOptions is the configuration options for StreamSigner.
|
||||||
|
type StreamSignerOptions struct{}
|
||||||
|
|
||||||
|
// StreamSigner implements Signature Version 4 (SigV4) signing of event stream encoded payloads.
|
||||||
|
type StreamSigner struct {
|
||||||
|
options StreamSignerOptions
|
||||||
|
|
||||||
|
credentials Credentials
|
||||||
|
service string
|
||||||
|
|
||||||
|
prevSignature []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStreamSigner returns a new AWS EventStream protocol signer.
|
||||||
|
func NewStreamSigner(credentials Credentials, service string, seedSignature []byte, optFns ...func(*StreamSignerOptions)) *StreamSigner {
|
||||||
|
o := StreamSignerOptions{}
|
||||||
|
|
||||||
|
for _, fn := range optFns {
|
||||||
|
fn(&o)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &StreamSigner{
|
||||||
|
options: o,
|
||||||
|
credentials: credentials,
|
||||||
|
service: service,
|
||||||
|
prevSignature: seedSignature,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StreamSigner) VerifySignature(headers, payload []byte, signingTime time.Time, signature []byte, optFns ...func(*StreamSignerOptions)) error {
|
||||||
|
options := s.options
|
||||||
|
|
||||||
|
for _, fn := range optFns {
|
||||||
|
fn(&options)
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSignature := s.prevSignature
|
||||||
|
|
||||||
|
st := v4Internal.NewSigningTime(signingTime)
|
||||||
|
|
||||||
|
scope := buildCredentialScope(st, s.service)
|
||||||
|
|
||||||
|
stringToSign := s.buildEventStreamStringToSign(headers, payload, prevSignature, scope, &st)
|
||||||
|
|
||||||
|
ok, err := signerCrypto.VerifySignature(&s.credentials.PrivateKey.PublicKey, makeHash(sha256.New(), []byte(stringToSign)), signature)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("v4a: invalid signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.prevSignature = signature
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StreamSigner) buildEventStreamStringToSign(headers, payload, previousSignature []byte, credentialScope string, signingTime *v4Internal.SigningTime) string {
|
||||||
|
hash := sha256.New()
|
||||||
|
return strings.Join([]string{
|
||||||
|
"AWS4-ECDSA-P256-SHA256-PAYLOAD",
|
||||||
|
signingTime.TimeFormat(),
|
||||||
|
credentialScope,
|
||||||
|
hex.EncodeToString(previousSignature),
|
||||||
|
hex.EncodeToString(makeHash(hash, headers)),
|
||||||
|
hex.EncodeToString(makeHash(hash, payload)),
|
||||||
|
}, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCredentialScope(st v4Internal.SigningTime, service string) string {
|
||||||
|
return strings.Join([]string{
|
||||||
|
st.Format(shortTimeFormat),
|
||||||
|
service,
|
||||||
|
"aws4_request",
|
||||||
|
}, "/")
|
||||||
|
|
||||||
|
}
|
577
api/auth/signer/v4asdk2/v4a.go
Normal file
577
api/auth/signer/v4asdk2/v4a.go
Normal file
|
@ -0,0 +1,577 @@
|
||||||
|
package v4a
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
"math/big"
|
||||||
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
|
"net/url"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
signerCrypto "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2/internal/crypto"
|
||||||
|
v4Internal "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2/internal/v4"
|
||||||
|
"github.com/aws/smithy-go/encoding/httpbinding"
|
||||||
|
"github.com/aws/smithy-go/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// AmzRegionSetKey represents the region set header used for sigv4a
|
||||||
|
AmzRegionSetKey = "X-Amz-Region-Set"
|
||||||
|
amzAlgorithmKey = v4Internal.AmzAlgorithmKey
|
||||||
|
amzSecurityTokenKey = v4Internal.AmzSecurityTokenKey
|
||||||
|
amzDateKey = v4Internal.AmzDateKey
|
||||||
|
amzCredentialKey = v4Internal.AmzCredentialKey
|
||||||
|
amzSignedHeadersKey = v4Internal.AmzSignedHeadersKey
|
||||||
|
authorizationHeader = "Authorization"
|
||||||
|
|
||||||
|
signingAlgorithm = "AWS4-ECDSA-P256-SHA256"
|
||||||
|
|
||||||
|
timeFormat = "20060102T150405Z"
|
||||||
|
shortTimeFormat = "20060102"
|
||||||
|
|
||||||
|
// EmptyStringSHA256 is a hex encoded SHA-256 hash of an empty string
|
||||||
|
EmptyStringSHA256 = v4Internal.EmptyStringSHA256
|
||||||
|
|
||||||
|
// Version of signing v4a
|
||||||
|
Version = "SigV4A"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
p256 elliptic.Curve
|
||||||
|
nMinusTwoP256 *big.Int
|
||||||
|
|
||||||
|
one = new(big.Int).SetInt64(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Ensure the elliptic curve parameters are initialized on package import rather then on first usage
|
||||||
|
p256 = elliptic.P256()
|
||||||
|
|
||||||
|
nMinusTwoP256 = new(big.Int).SetBytes(p256.Params().N.Bytes())
|
||||||
|
nMinusTwoP256 = nMinusTwoP256.Sub(nMinusTwoP256, new(big.Int).SetInt64(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignerOptions is the SigV4a signing options for constructing a Signer.
|
||||||
|
type SignerOptions struct {
|
||||||
|
Logger logging.Logger
|
||||||
|
LogSigning bool
|
||||||
|
|
||||||
|
// Disables the Signer's moving HTTP header key/value pairs from the HTTP
|
||||||
|
// request header to the request's query string. This is most commonly used
|
||||||
|
// with pre-signed requests preventing headers from being added to the
|
||||||
|
// request's query string.
|
||||||
|
DisableHeaderHoisting bool
|
||||||
|
|
||||||
|
// Disables the automatic escaping of the URI path of the request for the
|
||||||
|
// siganture's canonical string's path. For services that do not need additional
|
||||||
|
// escaping then use this to disable the signer escaping the path.
|
||||||
|
//
|
||||||
|
// S3 is an example of a service that does not need additional escaping.
|
||||||
|
//
|
||||||
|
// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
||||||
|
DisableURIPathEscaping bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signer is a SigV4a HTTP signing implementation
|
||||||
|
type Signer struct {
|
||||||
|
options SignerOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSigner constructs a SigV4a Signer.
|
||||||
|
func NewSigner(optFns ...func(*SignerOptions)) *Signer {
|
||||||
|
options := SignerOptions{}
|
||||||
|
|
||||||
|
for _, fn := range optFns {
|
||||||
|
fn(&options)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Signer{options: options}
|
||||||
|
}
|
||||||
|
|
||||||
|
// deriveKeyFromAccessKeyPair derives a NIST P-256 PrivateKey from the given
|
||||||
|
// IAM AccessKey and SecretKey pair.
|
||||||
|
//
|
||||||
|
// Based on FIPS.186-4 Appendix B.4.2
|
||||||
|
func deriveKeyFromAccessKeyPair(accessKey, secretKey string) (*ecdsa.PrivateKey, error) {
|
||||||
|
params := p256.Params()
|
||||||
|
bitLen := params.BitSize // Testing random candidates does not require an additional 64 bits
|
||||||
|
counter := 0x01
|
||||||
|
|
||||||
|
buffer := make([]byte, 1+len(accessKey)) // 1 byte counter + len(accessKey)
|
||||||
|
kdfContext := bytes.NewBuffer(buffer)
|
||||||
|
|
||||||
|
inputKey := append([]byte("AWS4A"), []byte(secretKey)...)
|
||||||
|
|
||||||
|
d := new(big.Int)
|
||||||
|
for {
|
||||||
|
kdfContext.Reset()
|
||||||
|
kdfContext.WriteString(accessKey)
|
||||||
|
kdfContext.WriteByte(byte(counter))
|
||||||
|
|
||||||
|
key, err := signerCrypto.HMACKeyDerivation(sha256.New, bitLen, inputKey, []byte(signingAlgorithm), kdfContext.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check key first before calling SetBytes if key key is in fact a valid candidate.
|
||||||
|
// This ensures the byte slice is the correct length (32-bytes) to compare in constant-time
|
||||||
|
cmp, err := signerCrypto.ConstantTimeByteCompare(key, nMinusTwoP256.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if cmp == -1 {
|
||||||
|
d.SetBytes(key)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
counter++
|
||||||
|
if counter > 0xFF {
|
||||||
|
return nil, fmt.Errorf("exhausted single byte external counter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d = d.Add(d, one)
|
||||||
|
|
||||||
|
priv := new(ecdsa.PrivateKey)
|
||||||
|
priv.PublicKey.Curve = p256
|
||||||
|
priv.D = d
|
||||||
|
priv.PublicKey.X, priv.PublicKey.Y = p256.ScalarBaseMult(d.Bytes())
|
||||||
|
|
||||||
|
return priv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpSigner struct {
|
||||||
|
Request *http.Request
|
||||||
|
ServiceName string
|
||||||
|
RegionSet []string
|
||||||
|
Time time.Time
|
||||||
|
Credentials Credentials
|
||||||
|
IsPreSign bool
|
||||||
|
|
||||||
|
Logger logging.Logger
|
||||||
|
Debug bool
|
||||||
|
|
||||||
|
// PayloadHash is the hex encoded SHA-256 hash of the request payload
|
||||||
|
// If len(PayloadHash) == 0 the signer will attempt to send the request
|
||||||
|
// as an unsigned payload. Note: Unsigned payloads only work for a subset of services.
|
||||||
|
PayloadHash string
|
||||||
|
|
||||||
|
DisableHeaderHoisting bool
|
||||||
|
DisableURIPathEscaping bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignHTTP takes the provided http.Request, payload hash, service, regionSet, and time and signs using SigV4a.
|
||||||
|
// The passed in request will be modified in place.
|
||||||
|
func (s *Signer) SignHTTP(ctx context.Context, credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, optFns ...func(*SignerOptions)) error {
|
||||||
|
options := s.options
|
||||||
|
for _, fn := range optFns {
|
||||||
|
fn(&options)
|
||||||
|
}
|
||||||
|
|
||||||
|
signer := &httpSigner{
|
||||||
|
Request: r,
|
||||||
|
PayloadHash: payloadHash,
|
||||||
|
ServiceName: service,
|
||||||
|
RegionSet: regionSet,
|
||||||
|
Credentials: credentials,
|
||||||
|
Time: signingTime.UTC(),
|
||||||
|
DisableHeaderHoisting: options.DisableHeaderHoisting,
|
||||||
|
DisableURIPathEscaping: options.DisableURIPathEscaping,
|
||||||
|
}
|
||||||
|
|
||||||
|
signedRequest, err := signer.Build()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logHTTPSigningInfo(ctx, options, signedRequest)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifySignature checks sigv4a.
|
||||||
|
func (s *Signer) VerifySignature(credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, signature string, optFns ...func(*SignerOptions)) error {
|
||||||
|
options := s.options
|
||||||
|
for _, fn := range optFns {
|
||||||
|
fn(&options)
|
||||||
|
}
|
||||||
|
|
||||||
|
signer := &httpSigner{
|
||||||
|
Request: r,
|
||||||
|
PayloadHash: payloadHash,
|
||||||
|
ServiceName: service,
|
||||||
|
RegionSet: regionSet,
|
||||||
|
Credentials: credentials,
|
||||||
|
Time: signingTime.UTC(),
|
||||||
|
DisableHeaderHoisting: options.DisableHeaderHoisting,
|
||||||
|
DisableURIPathEscaping: options.DisableURIPathEscaping,
|
||||||
|
}
|
||||||
|
|
||||||
|
signedReq, err := signer.Build()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logHTTPSigningInfo(context.TODO(), options, signedReq)
|
||||||
|
|
||||||
|
signatureRaw, err := hex.DecodeString(signature)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decode hex signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := signerCrypto.VerifySignature(&credentials.PrivateKey.PublicKey, makeHash(sha256.New(), []byte(signedReq.StringToSign)), signatureRaw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("v4a: invalid signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PresignHTTP takes the provided http.Request, payload hash, service, regionSet, and time and presigns using SigV4a
|
||||||
|
// Returns the presigned URL along with the headers that were signed with the request.
|
||||||
|
//
|
||||||
|
// PresignHTTP will not set the expires time of the presigned request
|
||||||
|
// automatically. To specify the expire duration for a request add the
|
||||||
|
// "X-Amz-Expires" query parameter on the request with the value as the
|
||||||
|
// duration in seconds the presigned URL should be considered valid for. This
|
||||||
|
// parameter is not used by all AWS services, and is most notable used by
|
||||||
|
// Amazon S3 APIs.
|
||||||
|
func (s *Signer) PresignHTTP(ctx context.Context, credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, optFns ...func(*SignerOptions)) (signedURI string, signedHeaders http.Header, err error) {
|
||||||
|
options := s.options
|
||||||
|
for _, fn := range optFns {
|
||||||
|
fn(&options)
|
||||||
|
}
|
||||||
|
|
||||||
|
signer := &httpSigner{
|
||||||
|
Request: r,
|
||||||
|
PayloadHash: payloadHash,
|
||||||
|
ServiceName: service,
|
||||||
|
RegionSet: regionSet,
|
||||||
|
Credentials: credentials,
|
||||||
|
Time: signingTime.UTC(),
|
||||||
|
IsPreSign: true,
|
||||||
|
DisableHeaderHoisting: options.DisableHeaderHoisting,
|
||||||
|
DisableURIPathEscaping: options.DisableURIPathEscaping,
|
||||||
|
}
|
||||||
|
|
||||||
|
signedRequest, err := signer.Build()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logHTTPSigningInfo(ctx, options, signedRequest)
|
||||||
|
|
||||||
|
signedHeaders = make(http.Header)
|
||||||
|
|
||||||
|
// For the signed headers we canonicalize the header keys in the returned map.
|
||||||
|
// This avoids situations where can standard library double headers like host header. For example the standard
|
||||||
|
// library will set the Host header, even if it is present in lower-case form.
|
||||||
|
for k, v := range signedRequest.SignedHeaders {
|
||||||
|
key := textproto.CanonicalMIMEHeaderKey(k)
|
||||||
|
signedHeaders[key] = append(signedHeaders[key], v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return signedRequest.Request.URL.String(), signedHeaders, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpSigner) setRequiredSigningFields(headers http.Header, query url.Values) {
|
||||||
|
amzDate := s.Time.Format(timeFormat)
|
||||||
|
|
||||||
|
if s.IsPreSign {
|
||||||
|
query.Set(AmzRegionSetKey, strings.Join(s.RegionSet, ","))
|
||||||
|
query.Set(amzDateKey, amzDate)
|
||||||
|
query.Set(amzAlgorithmKey, signingAlgorithm)
|
||||||
|
if len(s.Credentials.SessionToken) > 0 {
|
||||||
|
query.Set(amzSecurityTokenKey, s.Credentials.SessionToken)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.Set(AmzRegionSetKey, strings.Join(s.RegionSet, ","))
|
||||||
|
headers.Set(amzDateKey, amzDate)
|
||||||
|
if len(s.Credentials.SessionToken) > 0 {
|
||||||
|
headers.Set(amzSecurityTokenKey, s.Credentials.SessionToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpSigner) Build() (signedRequest, error) {
|
||||||
|
req := s.Request
|
||||||
|
|
||||||
|
query := req.URL.Query()
|
||||||
|
headers := req.Header
|
||||||
|
|
||||||
|
s.setRequiredSigningFields(headers, query)
|
||||||
|
|
||||||
|
// Sort Each Query Key's Values
|
||||||
|
for key := range query {
|
||||||
|
sort.Strings(query[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
v4Internal.SanitizeHostForHeader(req)
|
||||||
|
|
||||||
|
credentialScope := s.buildCredentialScope()
|
||||||
|
credentialStr := s.Credentials.Context + "/" + credentialScope
|
||||||
|
if s.IsPreSign {
|
||||||
|
query.Set(amzCredentialKey, credentialStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
unsignedHeaders := headers
|
||||||
|
if s.IsPreSign && !s.DisableHeaderHoisting {
|
||||||
|
urlValues := url.Values{}
|
||||||
|
urlValues, unsignedHeaders = buildQuery(v4Internal.AllowedQueryHoisting, unsignedHeaders)
|
||||||
|
for k := range urlValues {
|
||||||
|
query[k] = urlValues[k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
host := req.URL.Host
|
||||||
|
if len(req.Host) > 0 {
|
||||||
|
host = req.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
signedHeaders http.Header
|
||||||
|
signedHeadersStr string
|
||||||
|
canonicalHeaderStr string
|
||||||
|
)
|
||||||
|
|
||||||
|
if s.IsPreSign {
|
||||||
|
signedHeaders, signedHeadersStr, canonicalHeaderStr = s.buildCanonicalHeaders(host, v4Internal.IgnoredPresignedHeaders, unsignedHeaders, s.Request.ContentLength)
|
||||||
|
} else {
|
||||||
|
signedHeaders, signedHeadersStr, canonicalHeaderStr = s.buildCanonicalHeaders(host, v4Internal.IgnoredHeaders, unsignedHeaders, s.Request.ContentLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.IsPreSign {
|
||||||
|
query.Set(amzSignedHeadersKey, signedHeadersStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawQuery := strings.Replace(query.Encode(), "+", "%20", -1)
|
||||||
|
|
||||||
|
canonicalURI := v4Internal.GetURIPath(req.URL)
|
||||||
|
if !s.DisableURIPathEscaping {
|
||||||
|
canonicalURI = httpbinding.EscapePath(canonicalURI, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
canonicalString := s.buildCanonicalString(
|
||||||
|
req.Method,
|
||||||
|
canonicalURI,
|
||||||
|
rawQuery,
|
||||||
|
signedHeadersStr,
|
||||||
|
canonicalHeaderStr,
|
||||||
|
)
|
||||||
|
|
||||||
|
strToSign := s.buildStringToSign(credentialScope, canonicalString)
|
||||||
|
signingSignature, err := s.buildSignature(strToSign)
|
||||||
|
if err != nil {
|
||||||
|
return signedRequest{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.IsPreSign {
|
||||||
|
rawQuery += "&X-Amz-Signature=" + signingSignature
|
||||||
|
} else {
|
||||||
|
headers[authorizationHeader] = append(headers[authorizationHeader][:0], buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature))
|
||||||
|
}
|
||||||
|
|
||||||
|
req.URL.RawQuery = rawQuery
|
||||||
|
|
||||||
|
return signedRequest{
|
||||||
|
Request: req,
|
||||||
|
SignedHeaders: signedHeaders,
|
||||||
|
CanonicalString: canonicalString,
|
||||||
|
StringToSign: strToSign,
|
||||||
|
PreSigned: s.IsPreSign,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature string) string {
|
||||||
|
const credential = "Credential="
|
||||||
|
const signedHeaders = "SignedHeaders="
|
||||||
|
const signature = "Signature="
|
||||||
|
const commaSpace = ", "
|
||||||
|
|
||||||
|
var parts strings.Builder
|
||||||
|
parts.Grow(len(signingAlgorithm) + 1 +
|
||||||
|
len(credential) + len(credentialStr) + len(commaSpace) +
|
||||||
|
len(signedHeaders) + len(signedHeadersStr) + len(commaSpace) +
|
||||||
|
len(signature) + len(signingSignature),
|
||||||
|
)
|
||||||
|
parts.WriteString(signingAlgorithm)
|
||||||
|
parts.WriteRune(' ')
|
||||||
|
parts.WriteString(credential)
|
||||||
|
parts.WriteString(credentialStr)
|
||||||
|
parts.WriteString(commaSpace)
|
||||||
|
parts.WriteString(signedHeaders)
|
||||||
|
parts.WriteString(signedHeadersStr)
|
||||||
|
parts.WriteString(commaSpace)
|
||||||
|
parts.WriteString(signature)
|
||||||
|
parts.WriteString(signingSignature)
|
||||||
|
return parts.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpSigner) buildCredentialScope() string {
|
||||||
|
return strings.Join([]string{
|
||||||
|
s.Time.Format(shortTimeFormat),
|
||||||
|
s.ServiceName,
|
||||||
|
"aws4_request",
|
||||||
|
}, "/")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildQuery(r v4Internal.Rule, header http.Header) (url.Values, http.Header) {
|
||||||
|
query := url.Values{}
|
||||||
|
unsignedHeaders := http.Header{}
|
||||||
|
for k, h := range header {
|
||||||
|
if r.IsValid(k) {
|
||||||
|
query[k] = h
|
||||||
|
} else {
|
||||||
|
unsignedHeaders[k] = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query, unsignedHeaders
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpSigner) buildCanonicalHeaders(host string, rule v4Internal.Rule, header http.Header, length int64) (signed http.Header, signedHeaders, canonicalHeadersStr string) {
|
||||||
|
signed = make(http.Header)
|
||||||
|
|
||||||
|
var headers []string
|
||||||
|
const hostHeader = "host"
|
||||||
|
headers = append(headers, hostHeader)
|
||||||
|
signed[hostHeader] = append(signed[hostHeader], host)
|
||||||
|
|
||||||
|
const contentLengthHeader = "content-length"
|
||||||
|
if length > 0 {
|
||||||
|
headers = append(headers, contentLengthHeader)
|
||||||
|
signed[contentLengthHeader] = append(signed[contentLengthHeader], strconv.FormatInt(length, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range header {
|
||||||
|
if !rule.IsValid(k) {
|
||||||
|
continue // ignored header
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.EqualFold(k, contentLengthHeader) {
|
||||||
|
// prevent signing already handled content-length header.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lowerCaseKey := strings.ToLower(k)
|
||||||
|
if _, ok := signed[lowerCaseKey]; ok {
|
||||||
|
// include additional values
|
||||||
|
signed[lowerCaseKey] = append(signed[lowerCaseKey], v...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = append(headers, lowerCaseKey)
|
||||||
|
signed[lowerCaseKey] = v
|
||||||
|
}
|
||||||
|
sort.Strings(headers)
|
||||||
|
|
||||||
|
signedHeaders = strings.Join(headers, ";")
|
||||||
|
|
||||||
|
var canonicalHeaders strings.Builder
|
||||||
|
n := len(headers)
|
||||||
|
const colon = ':'
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if headers[i] == hostHeader {
|
||||||
|
canonicalHeaders.WriteString(hostHeader)
|
||||||
|
canonicalHeaders.WriteRune(colon)
|
||||||
|
canonicalHeaders.WriteString(v4Internal.StripExcessSpaces(host))
|
||||||
|
} else {
|
||||||
|
canonicalHeaders.WriteString(headers[i])
|
||||||
|
canonicalHeaders.WriteRune(colon)
|
||||||
|
// Trim out leading, trailing, and dedup inner spaces from signed header values.
|
||||||
|
values := signed[headers[i]]
|
||||||
|
for j, v := range values {
|
||||||
|
cleanedValue := strings.TrimSpace(v4Internal.StripExcessSpaces(v))
|
||||||
|
canonicalHeaders.WriteString(cleanedValue)
|
||||||
|
if j < len(values)-1 {
|
||||||
|
canonicalHeaders.WriteRune(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canonicalHeaders.WriteRune('\n')
|
||||||
|
}
|
||||||
|
canonicalHeadersStr = canonicalHeaders.String()
|
||||||
|
|
||||||
|
return signed, signedHeaders, canonicalHeadersStr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpSigner) buildCanonicalString(method, uri, query, signedHeaders, canonicalHeaders string) string {
|
||||||
|
return strings.Join([]string{
|
||||||
|
method,
|
||||||
|
uri,
|
||||||
|
query,
|
||||||
|
canonicalHeaders,
|
||||||
|
signedHeaders,
|
||||||
|
s.PayloadHash,
|
||||||
|
}, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpSigner) buildStringToSign(credentialScope, canonicalRequestString string) string {
|
||||||
|
return strings.Join([]string{
|
||||||
|
signingAlgorithm,
|
||||||
|
s.Time.Format(timeFormat),
|
||||||
|
credentialScope,
|
||||||
|
hex.EncodeToString(makeHash(sha256.New(), []byte(canonicalRequestString))),
|
||||||
|
}, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeHash(hash hash.Hash, b []byte) []byte {
|
||||||
|
hash.Reset()
|
||||||
|
hash.Write(b)
|
||||||
|
return hash.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpSigner) buildSignature(strToSign string) (string, error) {
|
||||||
|
sig, err := s.Credentials.PrivateKey.Sign(rand.Reader, makeHash(sha256.New(), []byte(strToSign)), crypto.SHA256)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(sig), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const logSignInfoMsg = `Request Signature:
|
||||||
|
---[ CANONICAL STRING ]-----------------------------
|
||||||
|
%s
|
||||||
|
---[ STRING TO SIGN ]--------------------------------
|
||||||
|
%s%s
|
||||||
|
-----------------------------------------------------`
|
||||||
|
const logSignedURLMsg = `
|
||||||
|
---[ SIGNED URL ]------------------------------------
|
||||||
|
%s`
|
||||||
|
|
||||||
|
func logHTTPSigningInfo(ctx context.Context, options SignerOptions, r signedRequest) {
|
||||||
|
if !options.LogSigning {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
signedURLMsg := ""
|
||||||
|
if r.PreSigned {
|
||||||
|
signedURLMsg = fmt.Sprintf(logSignedURLMsg, r.Request.URL.String())
|
||||||
|
}
|
||||||
|
logger := logging.WithContext(ctx, options.Logger)
|
||||||
|
logger.Logf(logging.Debug, logSignInfoMsg, r.CanonicalString, r.StringToSign, signedURLMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
type signedRequest struct {
|
||||||
|
Request *http.Request
|
||||||
|
SignedHeaders http.Header
|
||||||
|
CanonicalString string
|
||||||
|
StringToSign string
|
||||||
|
PreSigned bool
|
||||||
|
}
|
429
api/auth/signer/v4asdk2/v4a_test.go
Normal file
429
api/auth/signer/v4asdk2/v4a_test.go
Normal file
|
@ -0,0 +1,429 @@
|
||||||
|
package v4a
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2/internal/crypto"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
"github.com/aws/smithy-go/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
accessKey = "AKISORANDOMAASORANDOM"
|
||||||
|
secretKey = "q+jcrXGc+0zWN6uzclKVhvMmUsIfRPa4rlRandom"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDeriveECDSAKeyPairFromSecret(t *testing.T) {
|
||||||
|
privateKey, err := deriveKeyFromAccessKeyPair(accessKey, secretKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedX := func() *big.Int {
|
||||||
|
t.Helper()
|
||||||
|
b, ok := new(big.Int).SetString("15D242CEEBF8D8169FD6A8B5A746C41140414C3B07579038DA06AF89190FFFCB", 16)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("failed to parse big integer")
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}()
|
||||||
|
expectedY := func() *big.Int {
|
||||||
|
t.Helper()
|
||||||
|
b, ok := new(big.Int).SetString("515242CEDD82E94799482E4C0514B505AFCCF2C0C98D6A553BF539F424C5EC0", 16)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("failed to parse big integer")
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}()
|
||||||
|
|
||||||
|
if privateKey.X.Cmp(expectedX) != 0 {
|
||||||
|
t.Errorf("expected % X, got % X", expectedX, privateKey.X)
|
||||||
|
}
|
||||||
|
if privateKey.Y.Cmp(expectedY) != 0 {
|
||||||
|
t.Errorf("expected % X, got % X", expectedY, privateKey.Y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignHTTP(t *testing.T) {
|
||||||
|
req := buildRequest("dynamodb", "us-east-1")
|
||||||
|
|
||||||
|
signer, credProvider := buildSigner(t, true)
|
||||||
|
|
||||||
|
key, err := credProvider.RetrievePrivateKey(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expect no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = signer.SignHTTP(context.Background(), key, req, EmptyStringSHA256, "dynamodb", []string{"us-east-1"}, time.Unix(0, 0))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedDate := "19700101T000000Z"
|
||||||
|
expectedAlg := "AWS4-ECDSA-P256-SHA256"
|
||||||
|
expectedCredential := "AKISORANDOMAASORANDOM/19700101/dynamodb/aws4_request"
|
||||||
|
expectedSignedHeaders := "content-length;content-type;host;x-amz-date;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore;x-amz-region-set;x-amz-security-token;x-amz-target"
|
||||||
|
expectedStrToSignHash := "4ba7d0482cf4d5450cefdc067a00de1a4a715e444856fa3e1d85c35fb34d9730"
|
||||||
|
|
||||||
|
q := req.Header
|
||||||
|
|
||||||
|
validateAuthorization(t, q.Get("Authorization"), expectedAlg, expectedCredential, expectedSignedHeaders, expectedStrToSignHash)
|
||||||
|
|
||||||
|
if e, a := expectedDate, q.Get("X-Amz-Date"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignHTTP_NoSessionToken(t *testing.T) {
|
||||||
|
req := buildRequest("dynamodb", "us-east-1")
|
||||||
|
|
||||||
|
signer, credProvider := buildSigner(t, false)
|
||||||
|
|
||||||
|
key, err := credProvider.RetrievePrivateKey(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expect no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = signer.SignHTTP(context.Background(), key, req, EmptyStringSHA256, "dynamodb", []string{"us-east-1"}, time.Unix(0, 0))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedAlg := "AWS4-ECDSA-P256-SHA256"
|
||||||
|
expectedCredential := "AKISORANDOMAASORANDOM/19700101/dynamodb/aws4_request"
|
||||||
|
expectedSignedHeaders := "content-length;content-type;host;x-amz-date;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore;x-amz-region-set;x-amz-target"
|
||||||
|
expectedStrToSignHash := "1aeefb422ae6aa0de7aec829da813e55cff35553cac212dffd5f9474c71e47ee"
|
||||||
|
|
||||||
|
q := req.Header
|
||||||
|
|
||||||
|
validateAuthorization(t, q.Get("Authorization"), expectedAlg, expectedCredential, expectedSignedHeaders, expectedStrToSignHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPresignHTTP(t *testing.T) {
|
||||||
|
req := buildRequest("dynamodb", "us-east-1")
|
||||||
|
|
||||||
|
signer, credProvider := buildSigner(t, false)
|
||||||
|
|
||||||
|
key, err := credProvider.RetrievePrivateKey(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expect no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := req.URL.Query()
|
||||||
|
query.Set("X-Amz-Expires", "18000")
|
||||||
|
req.URL.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
signedURL, _, err := signer.PresignHTTP(context.Background(), key, req, EmptyStringSHA256, "dynamodb", []string{"us-east-1"}, time.Unix(0, 0))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedDate := "19700101T000000Z"
|
||||||
|
expectedAlg := "AWS4-ECDSA-P256-SHA256"
|
||||||
|
expectedHeaders := "content-length;content-type;host;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore"
|
||||||
|
expectedCredential := "AKISORANDOMAASORANDOM/19700101/dynamodb/aws4_request"
|
||||||
|
expectedStrToSignHash := "d7ffbd2fab644384c056957e6ac38de4ae68246764b5f5df171b3824153b6397"
|
||||||
|
expectedTarget := "prefix.Operation"
|
||||||
|
|
||||||
|
signedReq, err := url.Parse(signedURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := signedReq.Query()
|
||||||
|
|
||||||
|
validateSignature(t, expectedStrToSignHash, q.Get("X-Amz-Signature"))
|
||||||
|
|
||||||
|
if e, a := expectedAlg, q.Get("X-Amz-Algorithm"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := expectedCredential, q.Get("X-Amz-Credential"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := expectedHeaders, q.Get("X-Amz-SignedHeaders"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := expectedDate, q.Get("X-Amz-Date"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if a := q.Get("X-Amz-Meta-Other-Header"); len(a) != 0 {
|
||||||
|
t.Errorf("expect %v to be empty", a)
|
||||||
|
}
|
||||||
|
if e, a := expectedTarget, q.Get("X-Amz-Target"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := "us-east-1", q.Get("X-Amz-Region-Set"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPresignHTTP_BodyWithArrayRequest(t *testing.T) {
|
||||||
|
req := buildRequest("dynamodb", "us-east-1")
|
||||||
|
req.URL.RawQuery = "Foo=z&Foo=o&Foo=m&Foo=a"
|
||||||
|
|
||||||
|
signer, credProvider := buildSigner(t, true)
|
||||||
|
|
||||||
|
key, err := credProvider.RetrievePrivateKey(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expect no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := req.URL.Query()
|
||||||
|
query.Set("X-Amz-Expires", "300")
|
||||||
|
req.URL.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
signedURI, _, err := signer.PresignHTTP(context.Background(), key, req, EmptyStringSHA256, "dynamodb", []string{"us-east-1"}, time.Unix(0, 0))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signedReq, err := url.Parse(signedURI)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedAlg := "AWS4-ECDSA-P256-SHA256"
|
||||||
|
expectedDate := "19700101T000000Z"
|
||||||
|
expectedHeaders := "content-length;content-type;host;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore"
|
||||||
|
expectedStrToSignHash := "acff64fd3689be96259d4112c3742ff79f4da0d813bc58a285dc1c4449760bec"
|
||||||
|
expectedCred := "AKISORANDOMAASORANDOM/19700101/dynamodb/aws4_request"
|
||||||
|
expectedTarget := "prefix.Operation"
|
||||||
|
|
||||||
|
q := signedReq.Query()
|
||||||
|
|
||||||
|
validateSignature(t, expectedStrToSignHash, q.Get("X-Amz-Signature"))
|
||||||
|
|
||||||
|
if e, a := expectedAlg, q.Get("X-Amz-Algorithm"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := expectedCred, q.Get("X-Amz-Credential"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := expectedHeaders, q.Get("X-Amz-SignedHeaders"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := expectedDate, q.Get("X-Amz-Date"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if a := q.Get("X-Amz-Meta-Other-Header"); len(a) != 0 {
|
||||||
|
t.Errorf("expect %v to be empty, was not", a)
|
||||||
|
}
|
||||||
|
if e, a := expectedTarget, q.Get("X-Amz-Target"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := "us-east-1", q.Get("X-Amz-Region-Set"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestSign_buildCanonicalHeaders(t *testing.T) {
|
||||||
|
serviceName := "mockAPI"
|
||||||
|
region := "mock-region"
|
||||||
|
endpoint := "https://" + serviceName + "." + region + ".amazonaws.com"
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create request, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("FooInnerSpace", " inner space ")
|
||||||
|
req.Header.Set("FooLeadingSpace", " leading-space")
|
||||||
|
req.Header.Add("FooMultipleSpace", "no-space")
|
||||||
|
req.Header.Add("FooMultipleSpace", "\ttab-space")
|
||||||
|
req.Header.Add("FooMultipleSpace", "trailing-space ")
|
||||||
|
req.Header.Set("FooNoSpace", "no-space")
|
||||||
|
req.Header.Set("FooTabSpace", "\ttab-space\t")
|
||||||
|
req.Header.Set("FooTrailingSpace", "trailing-space ")
|
||||||
|
req.Header.Set("FooWrappedSpace", " wrapped-space ")
|
||||||
|
|
||||||
|
credProvider := &SymmetricCredentialAdaptor{
|
||||||
|
SymmetricProvider: staticCredentialsProvider{
|
||||||
|
Value: aws.Credentials{
|
||||||
|
AccessKeyID: accessKey,
|
||||||
|
SecretAccessKey: secretKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
key, err := credProvider.RetrievePrivateKey(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expect no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := &httpSigner{
|
||||||
|
Request: req,
|
||||||
|
ServiceName: serviceName,
|
||||||
|
RegionSet: []string{region},
|
||||||
|
Credentials: key,
|
||||||
|
Time: time.Date(2021, 10, 20, 12, 42, 0, 0, time.UTC),
|
||||||
|
}
|
||||||
|
|
||||||
|
build, err := ctx.Build()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectCanonicalString := strings.Join([]string{
|
||||||
|
`POST`,
|
||||||
|
`/`,
|
||||||
|
``,
|
||||||
|
`fooinnerspace:inner space`,
|
||||||
|
`fooleadingspace:leading-space`,
|
||||||
|
`foomultiplespace:no-space,tab-space,trailing-space`,
|
||||||
|
`foonospace:no-space`,
|
||||||
|
`footabspace:tab-space`,
|
||||||
|
`footrailingspace:trailing-space`,
|
||||||
|
`foowrappedspace:wrapped-space`,
|
||||||
|
`host:mockAPI.mock-region.amazonaws.com`,
|
||||||
|
`x-amz-date:20211020T124200Z`,
|
||||||
|
`x-amz-region-set:mock-region`,
|
||||||
|
``,
|
||||||
|
`fooinnerspace;fooleadingspace;foomultiplespace;foonospace;footabspace;footrailingspace;foowrappedspace;host;x-amz-date;x-amz-region-set`,
|
||||||
|
``,
|
||||||
|
}, "\n")
|
||||||
|
if diff := cmpDiff(expectCanonicalString, build.CanonicalString); diff != "" {
|
||||||
|
t.Errorf("expect match, got\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAuthorization(t *testing.T, authorization, expectedAlg, expectedCredential, expectedSignedHeaders, expectedStrToSignHash string) {
|
||||||
|
t.Helper()
|
||||||
|
split := strings.SplitN(authorization, " ", 2)
|
||||||
|
|
||||||
|
if len(split) != 2 {
|
||||||
|
t.Fatal("unexpected authorization header format")
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, a := split[0], expectedAlg; e != a {
|
||||||
|
t.Errorf("expected %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyValues := strings.Split(split[1], ", ")
|
||||||
|
seen := make(map[string]string)
|
||||||
|
|
||||||
|
for _, kv := range keyValues {
|
||||||
|
idx := strings.Index(kv, "=")
|
||||||
|
if idx == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key, value := kv[:idx], kv[idx+1:]
|
||||||
|
seen[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if a, ok := seen["Credential"]; ok {
|
||||||
|
if expectedCredential != a {
|
||||||
|
t.Errorf("expected credential %v, got %v", expectedCredential, a)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Errorf("Credential not found in authorization string")
|
||||||
|
}
|
||||||
|
|
||||||
|
if a, ok := seen["SignedHeaders"]; ok {
|
||||||
|
if expectedSignedHeaders != a {
|
||||||
|
t.Errorf("expected signed headers %v, got %v", expectedSignedHeaders, a)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Errorf("SignedHeaders not found in authorization string")
|
||||||
|
}
|
||||||
|
|
||||||
|
if a, ok := seen["Signature"]; ok {
|
||||||
|
validateSignature(t, expectedStrToSignHash, a)
|
||||||
|
} else {
|
||||||
|
t.Errorf("signature not found in authorization string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateSignature(t *testing.T, expectedHash, signature string) {
|
||||||
|
t.Helper()
|
||||||
|
pair, err := deriveKeyFromAccessKeyPair(accessKey, secretKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, _ := hex.DecodeString(expectedHash)
|
||||||
|
sig, _ := hex.DecodeString(signature)
|
||||||
|
|
||||||
|
ok, err := crypto.VerifySignature(&pair.PublicKey, hash, sig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("failed to verify signing singature")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildRequest(serviceName, region string) *http.Request {
|
||||||
|
endpoint := "https://" + serviceName + "." + region + ".amazonaws.com"
|
||||||
|
req, _ := http.NewRequest("POST", endpoint, nil)
|
||||||
|
req.URL.Opaque = "//example.org/bucket/key-._~,!@%23$%25^&*()"
|
||||||
|
req.Header.Set("X-Amz-Target", "prefix.Operation")
|
||||||
|
req.Header.Set("Content-Type", "application/x-amz-json-1.0")
|
||||||
|
|
||||||
|
req.Header.Set("Content-Length", strconv.Itoa(1024))
|
||||||
|
|
||||||
|
req.Header.Set("X-Amz-Meta-Other-Header", "some-value=!@#$%^&* (+)")
|
||||||
|
req.Header.Add("X-Amz-Meta-Other-Header_With_Underscore", "some-value=!@#$%^&* (+)")
|
||||||
|
req.Header.Add("X-amz-Meta-Other-Header_With_Underscore", "some-value=!@#$%^&* (+)")
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSigner(t *testing.T, withToken bool) (*Signer, CredentialsProvider) {
|
||||||
|
creds := aws.Credentials{
|
||||||
|
AccessKeyID: accessKey,
|
||||||
|
SecretAccessKey: secretKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
if withToken {
|
||||||
|
creds.SessionToken = "TOKEN"
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewSigner(func(options *SignerOptions) {
|
||||||
|
options.Logger = loggerFunc(func(format string, v ...interface{}) {
|
||||||
|
t.Logf(format, v...)
|
||||||
|
})
|
||||||
|
}), &SymmetricCredentialAdaptor{
|
||||||
|
SymmetricProvider: staticCredentialsProvider{
|
||||||
|
Value: creds,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type loggerFunc func(format string, v ...interface{})
|
||||||
|
|
||||||
|
func (l loggerFunc) Logf(_ logging.Classification, format string, v ...interface{}) {
|
||||||
|
l(format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type staticCredentialsProvider struct {
|
||||||
|
Value aws.Credentials
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s staticCredentialsProvider) Retrieve(_ context.Context) (aws.Credentials, error) {
|
||||||
|
v := s.Value
|
||||||
|
if v.AccessKeyID == "" || v.SecretAccessKey == "" {
|
||||||
|
return aws.Credentials{
|
||||||
|
Source: "Source Name",
|
||||||
|
}, fmt.Errorf("static credentials are empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(v.Source) == 0 {
|
||||||
|
v.Source = "Source Name"
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmpDiff(e, a interface{}) string {
|
||||||
|
if !reflect.DeepEqual(e, a) {
|
||||||
|
return fmt.Sprintf("%v != %v", e, a)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
115
api/auth/signer/v4sdk2/signer/internal/v4/cache.go
Normal file
115
api/auth/signer/v4sdk2/signer/internal/v4/cache.go
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
)
|
||||||
|
|
||||||
|
func lookupKey(service, region string) string {
|
||||||
|
var s strings.Builder
|
||||||
|
s.Grow(len(region) + len(service) + 3)
|
||||||
|
s.WriteString(region)
|
||||||
|
s.WriteRune('/')
|
||||||
|
s.WriteString(service)
|
||||||
|
return s.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type derivedKey struct {
|
||||||
|
AccessKey string
|
||||||
|
Date time.Time
|
||||||
|
Credential []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type derivedKeyCache struct {
|
||||||
|
values map[string]derivedKey
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDerivedKeyCache() derivedKeyCache {
|
||||||
|
return derivedKeyCache{
|
||||||
|
values: make(map[string]derivedKey),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *derivedKeyCache) Get(credentials aws.Credentials, service, region string, signingTime SigningTime) []byte {
|
||||||
|
key := lookupKey(service, region)
|
||||||
|
s.mutex.RLock()
|
||||||
|
if cred, ok := s.get(key, credentials, signingTime.Time); ok {
|
||||||
|
s.mutex.RUnlock()
|
||||||
|
return cred
|
||||||
|
}
|
||||||
|
s.mutex.RUnlock()
|
||||||
|
|
||||||
|
s.mutex.Lock()
|
||||||
|
if cred, ok := s.get(key, credentials, signingTime.Time); ok {
|
||||||
|
s.mutex.Unlock()
|
||||||
|
return cred
|
||||||
|
}
|
||||||
|
cred := deriveKey(credentials.SecretAccessKey, service, region, signingTime)
|
||||||
|
entry := derivedKey{
|
||||||
|
AccessKey: credentials.AccessKeyID,
|
||||||
|
Date: signingTime.Time,
|
||||||
|
Credential: cred,
|
||||||
|
}
|
||||||
|
s.values[key] = entry
|
||||||
|
s.mutex.Unlock()
|
||||||
|
|
||||||
|
return cred
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *derivedKeyCache) get(key string, credentials aws.Credentials, signingTime time.Time) ([]byte, bool) {
|
||||||
|
cacheEntry, ok := s.retrieveFromCache(key)
|
||||||
|
if ok && cacheEntry.AccessKey == credentials.AccessKeyID && isSameDay(signingTime, cacheEntry.Date) {
|
||||||
|
return cacheEntry.Credential, true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *derivedKeyCache) retrieveFromCache(key string) (derivedKey, bool) {
|
||||||
|
if v, ok := s.values[key]; ok {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
return derivedKey{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SigningKeyDeriver derives a signing key from a set of credentials
|
||||||
|
type SigningKeyDeriver struct {
|
||||||
|
cache derivedKeyCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSigningKeyDeriver returns a new SigningKeyDeriver
|
||||||
|
func NewSigningKeyDeriver() *SigningKeyDeriver {
|
||||||
|
return &SigningKeyDeriver{
|
||||||
|
cache: newDerivedKeyCache(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeriveKey returns a derived signing key from the given credentials to be used with SigV4 signing.
|
||||||
|
func (k *SigningKeyDeriver) DeriveKey(credential aws.Credentials, service, region string, signingTime SigningTime) []byte {
|
||||||
|
return k.cache.Get(credential, service, region, signingTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deriveKey(secret, service, region string, t SigningTime) []byte {
|
||||||
|
hmacDate := HMACSHA256([]byte("AWS4"+secret), []byte(t.ShortTimeFormat()))
|
||||||
|
hmacRegion := HMACSHA256(hmacDate, []byte(region))
|
||||||
|
hmacService := HMACSHA256(hmacRegion, []byte(service))
|
||||||
|
return HMACSHA256(hmacService, []byte("aws4_request"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSameDay(x, y time.Time) bool {
|
||||||
|
xYear, xMonth, xDay := x.Date()
|
||||||
|
yYear, yMonth, yDay := y.Date()
|
||||||
|
|
||||||
|
if xYear != yYear {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if xMonth != yMonth {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return xDay == yDay
|
||||||
|
}
|
40
api/auth/signer/v4sdk2/signer/internal/v4/const.go
Normal file
40
api/auth/signer/v4sdk2/signer/internal/v4/const.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
// Signature Version 4 (SigV4) Constants
|
||||||
|
const (
|
||||||
|
// EmptyStringSHA256 is the hex encoded sha256 value of an empty string
|
||||||
|
EmptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
|
||||||
|
|
||||||
|
// UnsignedPayload indicates that the request payload body is unsigned
|
||||||
|
UnsignedPayload = "UNSIGNED-PAYLOAD"
|
||||||
|
|
||||||
|
// AmzAlgorithmKey indicates the signing algorithm
|
||||||
|
AmzAlgorithmKey = "X-Amz-Algorithm"
|
||||||
|
|
||||||
|
// AmzSecurityTokenKey indicates the security token to be used with temporary credentials
|
||||||
|
AmzSecurityTokenKey = "X-Amz-Security-Token"
|
||||||
|
|
||||||
|
// AmzDateKey is the UTC timestamp for the request in the format YYYYMMDD'T'HHMMSS'Z'
|
||||||
|
AmzDateKey = "X-Amz-Date"
|
||||||
|
|
||||||
|
// AmzCredentialKey is the access key ID and credential scope
|
||||||
|
AmzCredentialKey = "X-Amz-Credential"
|
||||||
|
|
||||||
|
// AmzSignedHeadersKey is the set of headers signed for the request
|
||||||
|
AmzSignedHeadersKey = "X-Amz-SignedHeaders"
|
||||||
|
|
||||||
|
// AmzSignatureKey is the query parameter to store the SigV4 signature
|
||||||
|
AmzSignatureKey = "X-Amz-Signature"
|
||||||
|
|
||||||
|
// TimeFormat is the time format to be used in the X-Amz-Date header or query parameter
|
||||||
|
TimeFormat = "20060102T150405Z"
|
||||||
|
|
||||||
|
// ShortTimeFormat is the shorten time format used in the credential scope
|
||||||
|
ShortTimeFormat = "20060102"
|
||||||
|
|
||||||
|
// ContentSHAKey is the SHA256 of request body
|
||||||
|
ContentSHAKey = "X-Amz-Content-Sha256"
|
||||||
|
|
||||||
|
// StreamingEventsPayload indicates that the request payload body is a signed event stream.
|
||||||
|
StreamingEventsPayload = "STREAMING-AWS4-HMAC-SHA256-EVENTS"
|
||||||
|
)
|
88
api/auth/signer/v4sdk2/signer/internal/v4/header_rules.go
Normal file
88
api/auth/signer/v4sdk2/signer/internal/v4/header_rules.go
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rules houses a set of Rule needed for validation of a
|
||||||
|
// string value
|
||||||
|
type Rules []Rule
|
||||||
|
|
||||||
|
// Rule interface allows for more flexible rules and just simply
|
||||||
|
// checks whether or not a value adheres to that Rule
|
||||||
|
type Rule interface {
|
||||||
|
IsValid(value string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid will iterate through all rules and see if any rules
|
||||||
|
// apply to the value and supports nested rules
|
||||||
|
func (r Rules) IsValid(value string) bool {
|
||||||
|
for _, rule := range r {
|
||||||
|
if rule.IsValid(value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapRule generic Rule for maps
|
||||||
|
type MapRule map[string]struct{}
|
||||||
|
|
||||||
|
// IsValid for the map Rule satisfies whether it exists in the map
|
||||||
|
func (m MapRule) IsValid(value string) bool {
|
||||||
|
_, ok := m[value]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowList is a generic Rule for include listing
|
||||||
|
type AllowList struct {
|
||||||
|
Rule
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid for AllowList checks if the value is within the AllowList
|
||||||
|
func (w AllowList) IsValid(value string) bool {
|
||||||
|
return w.Rule.IsValid(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExcludeList is a generic Rule for exclude listing
|
||||||
|
type ExcludeList struct {
|
||||||
|
Rule
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid for AllowList checks if the value is within the AllowList
|
||||||
|
func (b ExcludeList) IsValid(value string) bool {
|
||||||
|
return !b.Rule.IsValid(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patterns is a list of strings to match against
|
||||||
|
type Patterns []string
|
||||||
|
|
||||||
|
// IsValid for Patterns checks each pattern and returns if a match has
|
||||||
|
// been found
|
||||||
|
func (p Patterns) IsValid(value string) bool {
|
||||||
|
for _, pattern := range p {
|
||||||
|
if HasPrefixFold(value, pattern) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// InclusiveRules rules allow for rules to depend on one another
|
||||||
|
type InclusiveRules []Rule
|
||||||
|
|
||||||
|
// IsValid will return true if all rules are true
|
||||||
|
func (r InclusiveRules) IsValid(value string) bool {
|
||||||
|
for _, rule := range r {
|
||||||
|
if !rule.IsValid(value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasPrefixFold tests whether the string s begins with prefix, interpreted as UTF-8 strings,
|
||||||
|
// under Unicode case-folding.
|
||||||
|
func HasPrefixFold(s, prefix string) bool {
|
||||||
|
return len(s) >= len(prefix) && strings.EqualFold(s[0:len(prefix)], prefix)
|
||||||
|
}
|
84
api/auth/signer/v4sdk2/signer/internal/v4/headers.go
Normal file
84
api/auth/signer/v4sdk2/signer/internal/v4/headers.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
// IgnoredPresignedHeaders is a list of headers that are ignored during signing
|
||||||
|
var IgnoredPresignedHeaders = Rules{
|
||||||
|
ExcludeList{
|
||||||
|
MapRule{
|
||||||
|
"Authorization": struct{}{},
|
||||||
|
"User-Agent": struct{}{},
|
||||||
|
"X-Amzn-Trace-Id": struct{}{},
|
||||||
|
"Expect": struct{}{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// IgnoredHeaders is a list of headers that are ignored during signing
|
||||||
|
// drop User-Agent header to be compatible with aws sdk java v1.
|
||||||
|
var IgnoredHeaders = Rules{
|
||||||
|
ExcludeList{
|
||||||
|
MapRule{
|
||||||
|
"Authorization": struct{}{},
|
||||||
|
//"User-Agent": struct{}{},
|
||||||
|
"X-Amzn-Trace-Id": struct{}{},
|
||||||
|
"Expect": struct{}{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequiredSignedHeaders is a allow list for Build canonical headers.
|
||||||
|
var RequiredSignedHeaders = Rules{
|
||||||
|
AllowList{
|
||||||
|
MapRule{
|
||||||
|
"Cache-Control": struct{}{},
|
||||||
|
"Content-Disposition": struct{}{},
|
||||||
|
"Content-Encoding": struct{}{},
|
||||||
|
"Content-Language": struct{}{},
|
||||||
|
"Content-Md5": struct{}{},
|
||||||
|
"Content-Type": struct{}{},
|
||||||
|
"Expires": struct{}{},
|
||||||
|
"If-Match": struct{}{},
|
||||||
|
"If-Modified-Since": struct{}{},
|
||||||
|
"If-None-Match": struct{}{},
|
||||||
|
"If-Unmodified-Since": struct{}{},
|
||||||
|
"Range": struct{}{},
|
||||||
|
"X-Amz-Acl": struct{}{},
|
||||||
|
"X-Amz-Copy-Source": struct{}{},
|
||||||
|
"X-Amz-Copy-Source-If-Match": struct{}{},
|
||||||
|
"X-Amz-Copy-Source-If-Modified-Since": struct{}{},
|
||||||
|
"X-Amz-Copy-Source-If-None-Match": struct{}{},
|
||||||
|
"X-Amz-Copy-Source-If-Unmodified-Since": struct{}{},
|
||||||
|
"X-Amz-Copy-Source-Range": struct{}{},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": struct{}{},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": struct{}{},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": struct{}{},
|
||||||
|
"X-Amz-Expected-Bucket-Owner": struct{}{},
|
||||||
|
"X-Amz-Grant-Full-control": struct{}{},
|
||||||
|
"X-Amz-Grant-Read": struct{}{},
|
||||||
|
"X-Amz-Grant-Read-Acp": struct{}{},
|
||||||
|
"X-Amz-Grant-Write": struct{}{},
|
||||||
|
"X-Amz-Grant-Write-Acp": struct{}{},
|
||||||
|
"X-Amz-Metadata-Directive": struct{}{},
|
||||||
|
"X-Amz-Mfa": struct{}{},
|
||||||
|
"X-Amz-Request-Payer": struct{}{},
|
||||||
|
"X-Amz-Server-Side-Encryption": struct{}{},
|
||||||
|
"X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": struct{}{},
|
||||||
|
"X-Amz-Server-Side-Encryption-Context": struct{}{},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": struct{}{},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key": struct{}{},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": struct{}{},
|
||||||
|
"X-Amz-Storage-Class": struct{}{},
|
||||||
|
"X-Amz-Website-Redirect-Location": struct{}{},
|
||||||
|
"X-Amz-Content-Sha256": struct{}{},
|
||||||
|
"X-Amz-Tagging": struct{}{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Patterns{"X-Amz-Object-Lock-"},
|
||||||
|
Patterns{"X-Amz-Meta-"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowedQueryHoisting is a allowed list for Build query headers. The boolean value
|
||||||
|
// represents whether or not it is a pattern.
|
||||||
|
var AllowedQueryHoisting = InclusiveRules{
|
||||||
|
ExcludeList{RequiredSignedHeaders},
|
||||||
|
Patterns{"X-Amz-"},
|
||||||
|
}
|
63
api/auth/signer/v4sdk2/signer/internal/v4/headers_test.go
Normal file
63
api/auth/signer/v4sdk2/signer/internal/v4/headers_test.go
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestAllowedQueryHoisting(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
Header string
|
||||||
|
ExpectHoist bool
|
||||||
|
}{
|
||||||
|
"object-lock": {
|
||||||
|
Header: "X-Amz-Object-Lock-Mode",
|
||||||
|
ExpectHoist: false,
|
||||||
|
},
|
||||||
|
"s3 metadata": {
|
||||||
|
Header: "X-Amz-Meta-SomeName",
|
||||||
|
ExpectHoist: false,
|
||||||
|
},
|
||||||
|
"another header": {
|
||||||
|
Header: "X-Amz-SomeOtherHeader",
|
||||||
|
ExpectHoist: true,
|
||||||
|
},
|
||||||
|
"non X-AMZ header": {
|
||||||
|
Header: "X-SomeOtherHeader",
|
||||||
|
ExpectHoist: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, c := range cases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
if e, a := c.ExpectHoist, AllowedQueryHoisting.IsValid(c.Header); e != a {
|
||||||
|
t.Errorf("expect hoist %v, was %v", e, a)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIgnoredHeaders(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
Header string
|
||||||
|
ExpectIgnored bool
|
||||||
|
}{
|
||||||
|
"expect": {
|
||||||
|
Header: "Expect",
|
||||||
|
ExpectIgnored: true,
|
||||||
|
},
|
||||||
|
"authorization": {
|
||||||
|
Header: "Authorization",
|
||||||
|
ExpectIgnored: true,
|
||||||
|
},
|
||||||
|
"X-AMZ header": {
|
||||||
|
Header: "X-Amz-Content-Sha256",
|
||||||
|
ExpectIgnored: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, c := range cases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
if e, a := c.ExpectIgnored, IgnoredHeaders.IsValid(c.Header); e == a {
|
||||||
|
t.Errorf("expect ignored %v, was %v", e, a)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
13
api/auth/signer/v4sdk2/signer/internal/v4/hmac.go
Normal file
13
api/auth/signer/v4sdk2/signer/internal/v4/hmac.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HMACSHA256 computes a HMAC-SHA256 of data given the provided key.
|
||||||
|
func HMACSHA256(key []byte, data []byte) []byte {
|
||||||
|
hash := hmac.New(sha256.New, key)
|
||||||
|
hash.Write(data)
|
||||||
|
return hash.Sum(nil)
|
||||||
|
}
|
75
api/auth/signer/v4sdk2/signer/internal/v4/host.go
Normal file
75
api/auth/signer/v4sdk2/signer/internal/v4/host.go
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SanitizeHostForHeader removes default port from host and updates request.Host
|
||||||
|
func SanitizeHostForHeader(r *http.Request) {
|
||||||
|
host := getHost(r)
|
||||||
|
port := portOnly(host)
|
||||||
|
if port != "" && isDefaultPort(r.URL.Scheme, port) {
|
||||||
|
r.Host = stripPort(host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns host from request
|
||||||
|
func getHost(r *http.Request) string {
|
||||||
|
if r.Host != "" {
|
||||||
|
return r.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.URL.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hostname returns u.Host, without any port number.
|
||||||
|
//
|
||||||
|
// If Host is an IPv6 literal with a port number, Hostname returns the
|
||||||
|
// IPv6 literal without the square brackets. IPv6 literals may include
|
||||||
|
// a zone identifier.
|
||||||
|
//
|
||||||
|
// Copied from the Go 1.8 standard library (net/url)
|
||||||
|
func stripPort(hostport string) string {
|
||||||
|
colon := strings.IndexByte(hostport, ':')
|
||||||
|
if colon == -1 {
|
||||||
|
return hostport
|
||||||
|
}
|
||||||
|
if i := strings.IndexByte(hostport, ']'); i != -1 {
|
||||||
|
return strings.TrimPrefix(hostport[:i], "[")
|
||||||
|
}
|
||||||
|
return hostport[:colon]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port returns the port part of u.Host, without the leading colon.
|
||||||
|
// If u.Host doesn't contain a port, Port returns an empty string.
|
||||||
|
//
|
||||||
|
// Copied from the Go 1.8 standard library (net/url)
|
||||||
|
func portOnly(hostport string) string {
|
||||||
|
colon := strings.IndexByte(hostport, ':')
|
||||||
|
if colon == -1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if i := strings.Index(hostport, "]:"); i != -1 {
|
||||||
|
return hostport[i+len("]:"):]
|
||||||
|
}
|
||||||
|
if strings.Contains(hostport, "]") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return hostport[colon+len(":"):]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if the specified URI is using the standard port
|
||||||
|
// (i.e. port 80 for HTTP URIs or 443 for HTTPS URIs)
|
||||||
|
func isDefaultPort(scheme, port string) bool {
|
||||||
|
if port == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
lowerCaseScheme := strings.ToLower(scheme)
|
||||||
|
if (lowerCaseScheme == "http" && port == "80") || (lowerCaseScheme == "https" && port == "443") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
13
api/auth/signer/v4sdk2/signer/internal/v4/scope.go
Normal file
13
api/auth/signer/v4sdk2/signer/internal/v4/scope.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// BuildCredentialScope builds the Signature Version 4 (SigV4) signing scope
|
||||||
|
func BuildCredentialScope(signingTime SigningTime, region, service string) string {
|
||||||
|
return strings.Join([]string{
|
||||||
|
signingTime.ShortTimeFormat(),
|
||||||
|
region,
|
||||||
|
service,
|
||||||
|
"aws4_request",
|
||||||
|
}, "/")
|
||||||
|
}
|
36
api/auth/signer/v4sdk2/signer/internal/v4/time.go
Normal file
36
api/auth/signer/v4sdk2/signer/internal/v4/time.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// SigningTime provides a wrapper around a time.Time which provides cached values for SigV4 signing.
|
||||||
|
type SigningTime struct {
|
||||||
|
time.Time
|
||||||
|
timeFormat string
|
||||||
|
shortTimeFormat string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSigningTime creates a new SigningTime given a time.Time
|
||||||
|
func NewSigningTime(t time.Time) SigningTime {
|
||||||
|
return SigningTime{
|
||||||
|
Time: t,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimeFormat provides a time formatted in the X-Amz-Date format.
|
||||||
|
func (m *SigningTime) TimeFormat() string {
|
||||||
|
return m.format(&m.timeFormat, TimeFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShortTimeFormat provides a time formatted of 20060102.
|
||||||
|
func (m *SigningTime) ShortTimeFormat() string {
|
||||||
|
return m.format(&m.shortTimeFormat, ShortTimeFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SigningTime) format(target *string, format string) string {
|
||||||
|
if len(*target) > 0 {
|
||||||
|
return *target
|
||||||
|
}
|
||||||
|
v := m.Time.Format(format)
|
||||||
|
*target = v
|
||||||
|
return v
|
||||||
|
}
|
80
api/auth/signer/v4sdk2/signer/internal/v4/util.go
Normal file
80
api/auth/signer/v4sdk2/signer/internal/v4/util.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const doubleSpace = " "
|
||||||
|
|
||||||
|
// StripExcessSpaces will rewrite the passed in slice's string values to not
|
||||||
|
// contain multiple side-by-side spaces.
|
||||||
|
func StripExcessSpaces(str string) string {
|
||||||
|
var j, k, l, m, spaces int
|
||||||
|
// Trim trailing spaces
|
||||||
|
for j = len(str) - 1; j >= 0 && str[j] == ' '; j-- {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim leading spaces
|
||||||
|
for k = 0; k < j && str[k] == ' '; k++ {
|
||||||
|
}
|
||||||
|
str = str[k : j+1]
|
||||||
|
|
||||||
|
// Strip multiple spaces.
|
||||||
|
j = strings.Index(str, doubleSpace)
|
||||||
|
if j < 0 {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := []byte(str)
|
||||||
|
for k, m, l = j, j, len(buf); k < l; k++ {
|
||||||
|
if buf[k] == ' ' {
|
||||||
|
if spaces == 0 {
|
||||||
|
// First space.
|
||||||
|
buf[m] = buf[k]
|
||||||
|
m++
|
||||||
|
}
|
||||||
|
spaces++
|
||||||
|
} else {
|
||||||
|
// End of multiple spaces.
|
||||||
|
spaces = 0
|
||||||
|
buf[m] = buf[k]
|
||||||
|
m++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(buf[:m])
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetURIPath returns the escaped URI component from the provided URL.
|
||||||
|
func GetURIPath(u *url.URL) string {
|
||||||
|
var uriPath string
|
||||||
|
|
||||||
|
if len(u.Opaque) > 0 {
|
||||||
|
const schemeSep, pathSep, queryStart = "//", "/", "?"
|
||||||
|
|
||||||
|
opaque := u.Opaque
|
||||||
|
// Cut off the query string if present.
|
||||||
|
if idx := strings.Index(opaque, queryStart); idx >= 0 {
|
||||||
|
opaque = opaque[:idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cutout the scheme separator if present.
|
||||||
|
if strings.Index(opaque, schemeSep) == 0 {
|
||||||
|
opaque = opaque[len(schemeSep):]
|
||||||
|
}
|
||||||
|
|
||||||
|
// capture URI path starting with first path separator.
|
||||||
|
if idx := strings.Index(opaque, pathSep); idx >= 0 {
|
||||||
|
uriPath = opaque[idx:]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uriPath = u.EscapedPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(uriPath) == 0 {
|
||||||
|
uriPath = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
return uriPath
|
||||||
|
}
|
158
api/auth/signer/v4sdk2/signer/internal/v4/util_test.go
Normal file
158
api/auth/signer/v4sdk2/signer/internal/v4/util_test.go
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func lazyURLParse(v string) func() (*url.URL, error) {
|
||||||
|
return func() (*url.URL, error) {
|
||||||
|
return url.Parse(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetURIPath(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
getURL func() (*url.URL, error)
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
// Cases
|
||||||
|
"with scheme": {
|
||||||
|
getURL: lazyURLParse("https://localhost:9000"),
|
||||||
|
expect: "/",
|
||||||
|
},
|
||||||
|
"no port, with scheme": {
|
||||||
|
getURL: lazyURLParse("https://localhost"),
|
||||||
|
expect: "/",
|
||||||
|
},
|
||||||
|
"without scheme": {
|
||||||
|
getURL: lazyURLParse("localhost:9000"),
|
||||||
|
expect: "/",
|
||||||
|
},
|
||||||
|
"without scheme, with path": {
|
||||||
|
getURL: lazyURLParse("localhost:9000/abc123"),
|
||||||
|
expect: "/abc123",
|
||||||
|
},
|
||||||
|
"without scheme, with separator": {
|
||||||
|
getURL: lazyURLParse("//localhost:9000"),
|
||||||
|
expect: "/",
|
||||||
|
},
|
||||||
|
"no port, without scheme, with separator": {
|
||||||
|
getURL: lazyURLParse("//localhost"),
|
||||||
|
expect: "/",
|
||||||
|
},
|
||||||
|
"without scheme, with separator, with path": {
|
||||||
|
getURL: lazyURLParse("//localhost:9000/abc123"),
|
||||||
|
expect: "/abc123",
|
||||||
|
},
|
||||||
|
"no port, without scheme, with separator, with path": {
|
||||||
|
getURL: lazyURLParse("//localhost/abc123"),
|
||||||
|
expect: "/abc123",
|
||||||
|
},
|
||||||
|
"opaque with query string": {
|
||||||
|
getURL: lazyURLParse("localhost:9000/abc123?efg=456"),
|
||||||
|
expect: "/abc123",
|
||||||
|
},
|
||||||
|
"failing test": {
|
||||||
|
getURL: func() (*url.URL, error) {
|
||||||
|
endpoint := "https://service.region.amazonaws.com"
|
||||||
|
req, _ := http.NewRequest("POST", endpoint, nil)
|
||||||
|
u := req.URL
|
||||||
|
|
||||||
|
u.Opaque = "//example.org/bucket/key-._~,!@#$%^&*()"
|
||||||
|
|
||||||
|
query := u.Query()
|
||||||
|
query.Set("some-query-key", "value")
|
||||||
|
u.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
},
|
||||||
|
expect: "/bucket/key-._~,!@#$%^&*()",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, c := range cases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
u, err := c.getURL()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get URL, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := GetURIPath(u)
|
||||||
|
if e, a := c.expect, actual; e != a {
|
||||||
|
t.Errorf("expect %v path, got %v", e, a)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStripExcessHeaders(t *testing.T) {
|
||||||
|
vals := []string{
|
||||||
|
"",
|
||||||
|
"123",
|
||||||
|
"1 2 3",
|
||||||
|
"1 2 3 ",
|
||||||
|
" 1 2 3",
|
||||||
|
"1 2 3",
|
||||||
|
"1 23",
|
||||||
|
"1 2 3",
|
||||||
|
"1 2 ",
|
||||||
|
" 1 2 ",
|
||||||
|
"12 3",
|
||||||
|
"12 3 1",
|
||||||
|
"12 3 1",
|
||||||
|
"12 3 1abc123",
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []string{
|
||||||
|
"",
|
||||||
|
"123",
|
||||||
|
"1 2 3",
|
||||||
|
"1 2 3",
|
||||||
|
"1 2 3",
|
||||||
|
"1 2 3",
|
||||||
|
"1 23",
|
||||||
|
"1 2 3",
|
||||||
|
"1 2",
|
||||||
|
"1 2",
|
||||||
|
"12 3",
|
||||||
|
"12 3 1",
|
||||||
|
"12 3 1",
|
||||||
|
"12 3 1abc123",
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(vals); i++ {
|
||||||
|
r := StripExcessSpaces(vals[i])
|
||||||
|
if e, a := expected[i], r; e != a {
|
||||||
|
t.Errorf("%d, expect %v, got %v", i, e, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var stripExcessSpaceCases = []string{
|
||||||
|
`AWS4-HMAC-SHA256 Credential=AKIDFAKEIDFAKEID/20160628/us-west-2/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=1234567890abcdef1234567890abcdef1234567890abcdef`,
|
||||||
|
`123 321 123 321`,
|
||||||
|
` 123 321 123 321 `,
|
||||||
|
` 123 321 123 321 `,
|
||||||
|
"123",
|
||||||
|
"1 2 3",
|
||||||
|
" 1 2 3",
|
||||||
|
"1 2 3",
|
||||||
|
"1 23",
|
||||||
|
"1 2 3",
|
||||||
|
"1 2 ",
|
||||||
|
" 1 2 ",
|
||||||
|
"12 3",
|
||||||
|
"12 3 1",
|
||||||
|
"12 3 1",
|
||||||
|
"12 3 1abc123",
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkStripExcessSpaces(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
for _, v := range stripExcessSpaceCases {
|
||||||
|
StripExcessSpaces(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
442
api/auth/signer/v4sdk2/signer/v4/middleware.go
Normal file
442
api/auth/signer/v4sdk2/signer/v4/middleware.go
Normal file
|
@ -0,0 +1,442 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
v4Internal "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/internal/v4"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws/middleware/private/metrics"
|
||||||
|
"github.com/aws/smithy-go/middleware"
|
||||||
|
smithyhttp "github.com/aws/smithy-go/transport/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const computePayloadHashMiddlewareID = "ComputePayloadHash"
|
||||||
|
|
||||||
|
// HashComputationError indicates an error occurred while computing the signing hash
|
||||||
|
type HashComputationError struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error is the error message
|
||||||
|
func (e *HashComputationError) Error() string {
|
||||||
|
return fmt.Sprintf("failed to compute payload hash: %v", e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap returns the underlying error if one is set
|
||||||
|
func (e *HashComputationError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SigningError indicates an error condition occurred while performing SigV4 signing
|
||||||
|
type SigningError struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SigningError) Error() string {
|
||||||
|
return fmt.Sprintf("failed to sign request: %v", e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap returns the underlying error cause
|
||||||
|
func (e *SigningError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseDynamicPayloadSigningMiddleware swaps the compute payload sha256 middleware with a resolver middleware that
|
||||||
|
// switches between unsigned and signed payload based on TLS state for request.
|
||||||
|
// This middleware should not be used for AWS APIs that do not support unsigned payload signing auth.
|
||||||
|
// By default, SDK uses this middleware for known AWS APIs that support such TLS based auth selection .
|
||||||
|
//
|
||||||
|
// Usage example -
|
||||||
|
// S3 PutObject API allows unsigned payload signing auth usage when TLS is enabled, and uses this middleware to
|
||||||
|
// dynamically switch between unsigned and signed payload based on TLS state for request.
|
||||||
|
func UseDynamicPayloadSigningMiddleware(stack *middleware.Stack) error {
|
||||||
|
_, err := stack.Finalize.Swap(computePayloadHashMiddlewareID, &dynamicPayloadSigningMiddleware{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// dynamicPayloadSigningMiddleware dynamically resolves the middleware that computes and set payload sha256 middleware.
|
||||||
|
type dynamicPayloadSigningMiddleware struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns the resolver identifier
|
||||||
|
func (m *dynamicPayloadSigningMiddleware) ID() string {
|
||||||
|
return computePayloadHashMiddlewareID
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleFinalize delegates SHA256 computation according to whether the request
|
||||||
|
// is TLS-enabled.
|
||||||
|
func (m *dynamicPayloadSigningMiddleware) HandleFinalize(
|
||||||
|
ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler,
|
||||||
|
) (
|
||||||
|
out middleware.FinalizeOutput, metadata middleware.Metadata, err error,
|
||||||
|
) {
|
||||||
|
req, ok := in.Request.(*smithyhttp.Request)
|
||||||
|
if !ok {
|
||||||
|
return out, metadata, fmt.Errorf("unknown transport type %T", in.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.IsHTTPS() {
|
||||||
|
return (&UnsignedPayload{}).HandleFinalize(ctx, in, next)
|
||||||
|
}
|
||||||
|
return (&ComputePayloadSHA256{}).HandleFinalize(ctx, in, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsignedPayload sets the SigV4 request payload hash to unsigned.
|
||||||
|
//
|
||||||
|
// Will not set the Unsigned Payload magic SHA value, if a SHA has already been
|
||||||
|
// stored in the context. (e.g. application pre-computed SHA256 before making
|
||||||
|
// API call).
|
||||||
|
//
|
||||||
|
// This middleware does not check the X-Amz-Content-Sha256 header, if that
|
||||||
|
// header is serialized a middleware must translate it into the context.
|
||||||
|
type UnsignedPayload struct{}
|
||||||
|
|
||||||
|
// AddUnsignedPayloadMiddleware adds unsignedPayload to the operation
|
||||||
|
// middleware stack
|
||||||
|
func AddUnsignedPayloadMiddleware(stack *middleware.Stack) error {
|
||||||
|
return stack.Finalize.Insert(&UnsignedPayload{}, "ResolveEndpointV2", middleware.After)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns the unsignedPayload identifier
|
||||||
|
func (m *UnsignedPayload) ID() string {
|
||||||
|
return computePayloadHashMiddlewareID
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleFinalize sets the payload hash magic value to the unsigned sentinel.
|
||||||
|
func (m *UnsignedPayload) HandleFinalize(
|
||||||
|
ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler,
|
||||||
|
) (
|
||||||
|
out middleware.FinalizeOutput, metadata middleware.Metadata, err error,
|
||||||
|
) {
|
||||||
|
if GetPayloadHash(ctx) == "" {
|
||||||
|
ctx = SetPayloadHash(ctx, v4Internal.UnsignedPayload)
|
||||||
|
}
|
||||||
|
return next.HandleFinalize(ctx, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComputePayloadSHA256 computes SHA256 payload hash to sign.
|
||||||
|
//
|
||||||
|
// Will not set the Unsigned Payload magic SHA value, if a SHA has already been
|
||||||
|
// stored in the context. (e.g. application pre-computed SHA256 before making
|
||||||
|
// API call).
|
||||||
|
//
|
||||||
|
// This middleware does not check the X-Amz-Content-Sha256 header, if that
|
||||||
|
// header is serialized a middleware must translate it into the context.
|
||||||
|
type ComputePayloadSHA256 struct{}
|
||||||
|
|
||||||
|
// AddComputePayloadSHA256Middleware adds computePayloadSHA256 to the
|
||||||
|
// operation middleware stack
|
||||||
|
func AddComputePayloadSHA256Middleware(stack *middleware.Stack) error {
|
||||||
|
return stack.Finalize.Insert(&ComputePayloadSHA256{}, "ResolveEndpointV2", middleware.After)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveComputePayloadSHA256Middleware removes computePayloadSHA256 from the
|
||||||
|
// operation middleware stack
|
||||||
|
func RemoveComputePayloadSHA256Middleware(stack *middleware.Stack) error {
|
||||||
|
_, err := stack.Finalize.Remove(computePayloadHashMiddlewareID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID is the middleware name
|
||||||
|
func (m *ComputePayloadSHA256) ID() string {
|
||||||
|
return computePayloadHashMiddlewareID
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleFinalize computes the payload hash for the request, storing it to the
|
||||||
|
// context. This is a no-op if a caller has previously set that value.
|
||||||
|
func (m *ComputePayloadSHA256) HandleFinalize(
|
||||||
|
ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler,
|
||||||
|
) (
|
||||||
|
out middleware.FinalizeOutput, metadata middleware.Metadata, err error,
|
||||||
|
) {
|
||||||
|
if GetPayloadHash(ctx) != "" {
|
||||||
|
return next.HandleFinalize(ctx, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, ok := in.Request.(*smithyhttp.Request)
|
||||||
|
if !ok {
|
||||||
|
return out, metadata, &HashComputationError{
|
||||||
|
Err: fmt.Errorf("unexpected request middleware type %T", in.Request),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := sha256.New()
|
||||||
|
if stream := req.GetStream(); stream != nil {
|
||||||
|
_, err = io.Copy(hash, stream)
|
||||||
|
if err != nil {
|
||||||
|
return out, metadata, &HashComputationError{
|
||||||
|
Err: fmt.Errorf("failed to compute payload hash, %w", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := req.RewindStream(); err != nil {
|
||||||
|
return out, metadata, &HashComputationError{
|
||||||
|
Err: fmt.Errorf("failed to seek body to start, %w", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = SetPayloadHash(ctx, hex.EncodeToString(hash.Sum(nil)))
|
||||||
|
|
||||||
|
return next.HandleFinalize(ctx, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwapComputePayloadSHA256ForUnsignedPayloadMiddleware replaces the
|
||||||
|
// ComputePayloadSHA256 middleware with the UnsignedPayload middleware.
|
||||||
|
//
|
||||||
|
// Use this to disable computing the Payload SHA256 checksum and instead use
|
||||||
|
// UNSIGNED-PAYLOAD for the SHA256 value.
|
||||||
|
func SwapComputePayloadSHA256ForUnsignedPayloadMiddleware(stack *middleware.Stack) error {
|
||||||
|
_, err := stack.Finalize.Swap(computePayloadHashMiddlewareID, &UnsignedPayload{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContentSHA256Header sets the X-Amz-Content-Sha256 header value to
|
||||||
|
// the Payload hash stored in the context.
|
||||||
|
type ContentSHA256Header struct{}
|
||||||
|
|
||||||
|
// AddContentSHA256HeaderMiddleware adds ContentSHA256Header to the
|
||||||
|
// operation middleware stack
|
||||||
|
func AddContentSHA256HeaderMiddleware(stack *middleware.Stack) error {
|
||||||
|
return stack.Finalize.Insert(&ContentSHA256Header{}, computePayloadHashMiddlewareID, middleware.After)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveContentSHA256HeaderMiddleware removes contentSHA256Header middleware
|
||||||
|
// from the operation middleware stack
|
||||||
|
func RemoveContentSHA256HeaderMiddleware(stack *middleware.Stack) error {
|
||||||
|
_, err := stack.Finalize.Remove((*ContentSHA256Header)(nil).ID())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns the ContentSHA256HeaderMiddleware identifier
|
||||||
|
func (m *ContentSHA256Header) ID() string {
|
||||||
|
return "SigV4ContentSHA256Header"
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleFinalize sets the X-Amz-Content-Sha256 header value to the Payload hash
|
||||||
|
// stored in the context.
|
||||||
|
func (m *ContentSHA256Header) HandleFinalize(
|
||||||
|
ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler,
|
||||||
|
) (
|
||||||
|
out middleware.FinalizeOutput, metadata middleware.Metadata, err error,
|
||||||
|
) {
|
||||||
|
req, ok := in.Request.(*smithyhttp.Request)
|
||||||
|
if !ok {
|
||||||
|
return out, metadata, &HashComputationError{Err: fmt.Errorf("unexpected request middleware type %T", in.Request)}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set(v4Internal.ContentSHAKey, GetPayloadHash(ctx))
|
||||||
|
return next.HandleFinalize(ctx, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignHTTPRequestMiddlewareOptions is the configuration options for
|
||||||
|
// [SignHTTPRequestMiddleware].
|
||||||
|
//
|
||||||
|
// Deprecated: [SignHTTPRequestMiddleware] is deprecated.
|
||||||
|
type SignHTTPRequestMiddlewareOptions struct {
|
||||||
|
CredentialsProvider aws.CredentialsProvider
|
||||||
|
Signer HTTPSigner
|
||||||
|
LogSigning bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignHTTPRequestMiddleware is a `FinalizeMiddleware` implementation for SigV4
|
||||||
|
// HTTP Signing.
|
||||||
|
//
|
||||||
|
// Deprecated: AWS service clients no longer use this middleware. Signing as an
|
||||||
|
// SDK operation is now performed through an internal per-service middleware
|
||||||
|
// which opaquely selects and uses the signer from the resolved auth scheme.
|
||||||
|
type SignHTTPRequestMiddleware struct {
|
||||||
|
credentialsProvider aws.CredentialsProvider
|
||||||
|
signer HTTPSigner
|
||||||
|
logSigning bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSignHTTPRequestMiddleware constructs a [SignHTTPRequestMiddleware] using
|
||||||
|
// the given [Signer] for signing requests.
|
||||||
|
//
|
||||||
|
// Deprecated: SignHTTPRequestMiddleware is deprecated.
|
||||||
|
func NewSignHTTPRequestMiddleware(options SignHTTPRequestMiddlewareOptions) *SignHTTPRequestMiddleware {
|
||||||
|
return &SignHTTPRequestMiddleware{
|
||||||
|
credentialsProvider: options.CredentialsProvider,
|
||||||
|
signer: options.Signer,
|
||||||
|
logSigning: options.LogSigning,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID is the SignHTTPRequestMiddleware identifier.
|
||||||
|
//
|
||||||
|
// Deprecated: SignHTTPRequestMiddleware is deprecated.
|
||||||
|
func (s *SignHTTPRequestMiddleware) ID() string {
|
||||||
|
return "Signing"
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleFinalize will take the provided input and sign the request using the
|
||||||
|
// SigV4 authentication scheme.
|
||||||
|
//
|
||||||
|
// Deprecated: SignHTTPRequestMiddleware is deprecated.
|
||||||
|
func (s *SignHTTPRequestMiddleware) HandleFinalize(ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler) (
|
||||||
|
out middleware.FinalizeOutput, metadata middleware.Metadata, err error,
|
||||||
|
) {
|
||||||
|
if !haveCredentialProvider(s.credentialsProvider) {
|
||||||
|
return next.HandleFinalize(ctx, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, ok := in.Request.(*smithyhttp.Request)
|
||||||
|
if !ok {
|
||||||
|
return out, metadata, &SigningError{Err: fmt.Errorf("unexpected request middleware type %T", in.Request)}
|
||||||
|
}
|
||||||
|
|
||||||
|
signingName, signingRegion := awsmiddleware.GetSigningName(ctx), awsmiddleware.GetSigningRegion(ctx)
|
||||||
|
payloadHash := GetPayloadHash(ctx)
|
||||||
|
if len(payloadHash) == 0 {
|
||||||
|
return out, metadata, &SigningError{Err: fmt.Errorf("computed payload hash missing from context")}
|
||||||
|
}
|
||||||
|
|
||||||
|
mctx := metrics.Context(ctx)
|
||||||
|
|
||||||
|
if mctx != nil {
|
||||||
|
if attempt, err := mctx.Data().LatestAttempt(); err == nil {
|
||||||
|
attempt.CredentialFetchStartTime = time.Now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials, err := s.credentialsProvider.Retrieve(ctx)
|
||||||
|
|
||||||
|
if mctx != nil {
|
||||||
|
if attempt, err := mctx.Data().LatestAttempt(); err == nil {
|
||||||
|
attempt.CredentialFetchEndTime = time.Now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return out, metadata, &SigningError{Err: fmt.Errorf("failed to retrieve credentials: %w", err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
signerOptions := []func(o *SignerOptions){
|
||||||
|
func(o *SignerOptions) {
|
||||||
|
o.Logger = middleware.GetLogger(ctx)
|
||||||
|
o.LogSigning = s.logSigning
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// existing DisableURIPathEscaping is equivalent in purpose
|
||||||
|
// to authentication scheme property DisableDoubleEncoding
|
||||||
|
//disableDoubleEncoding, overridden := internalauth.GetDisableDoubleEncoding(ctx) // internalauth "github.com/aws/aws-sdk-go-v2/internal/auth"
|
||||||
|
//if overridden {
|
||||||
|
// signerOptions = append(signerOptions, func(o *SignerOptions) {
|
||||||
|
// o.DisableURIPathEscaping = disableDoubleEncoding
|
||||||
|
// })
|
||||||
|
//}
|
||||||
|
|
||||||
|
if mctx != nil {
|
||||||
|
if attempt, err := mctx.Data().LatestAttempt(); err == nil {
|
||||||
|
attempt.SignStartTime = time.Now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.signer.SignHTTP(ctx, credentials, req.Request, payloadHash, signingName, signingRegion, time.Now(), signerOptions...)
|
||||||
|
|
||||||
|
if mctx != nil {
|
||||||
|
if attempt, err := mctx.Data().LatestAttempt(); err == nil {
|
||||||
|
attempt.SignEndTime = time.Now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return out, metadata, &SigningError{Err: fmt.Errorf("failed to sign http request, %w", err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = awsmiddleware.SetSigningCredentials(ctx, credentials)
|
||||||
|
|
||||||
|
return next.HandleFinalize(ctx, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamingEventsPayload signs input event stream messages.
|
||||||
|
type StreamingEventsPayload struct{}
|
||||||
|
|
||||||
|
// AddStreamingEventsPayload adds the streamingEventsPayload middleware to the stack.
|
||||||
|
func AddStreamingEventsPayload(stack *middleware.Stack) error {
|
||||||
|
return stack.Finalize.Add(&StreamingEventsPayload{}, middleware.Before)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID identifies the middleware.
|
||||||
|
func (s *StreamingEventsPayload) ID() string {
|
||||||
|
return computePayloadHashMiddlewareID
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleFinalize marks the input stream to be signed with SigV4.
|
||||||
|
func (s *StreamingEventsPayload) HandleFinalize(
|
||||||
|
ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler,
|
||||||
|
) (
|
||||||
|
out middleware.FinalizeOutput, metadata middleware.Metadata, err error,
|
||||||
|
) {
|
||||||
|
contentSHA := GetPayloadHash(ctx)
|
||||||
|
if len(contentSHA) == 0 {
|
||||||
|
contentSHA = v4Internal.StreamingEventsPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = SetPayloadHash(ctx, contentSHA)
|
||||||
|
|
||||||
|
return next.HandleFinalize(ctx, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSignedRequestSignature attempts to extract the signature of the request.
|
||||||
|
// Returning an error if the request is unsigned, or unable to extract the
|
||||||
|
// signature.
|
||||||
|
func GetSignedRequestSignature(r *http.Request) ([]byte, error) {
|
||||||
|
const authHeaderSignatureElem = "Signature="
|
||||||
|
|
||||||
|
if auth := r.Header.Get(authorizationHeader); len(auth) != 0 {
|
||||||
|
ps := strings.Split(auth, ", ")
|
||||||
|
for _, p := range ps {
|
||||||
|
if idx := strings.Index(p, authHeaderSignatureElem); idx >= 0 {
|
||||||
|
sig := p[len(authHeaderSignatureElem):]
|
||||||
|
if len(sig) == 0 {
|
||||||
|
return nil, fmt.Errorf("invalid request signature authorization header")
|
||||||
|
}
|
||||||
|
return hex.DecodeString(sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sig := r.URL.Query().Get("X-Amz-Signature"); len(sig) != 0 {
|
||||||
|
return hex.DecodeString(sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("request not signed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func haveCredentialProvider(p aws.CredentialsProvider) bool {
|
||||||
|
if p == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return !aws.IsCredentialsProvider(p, (*aws.AnonymousCredentials)(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
type payloadHashKey struct{}
|
||||||
|
|
||||||
|
// GetPayloadHash retrieves the payload hash to use for signing
|
||||||
|
//
|
||||||
|
// Scoped to stack values. Use github.com/aws/smithy-go/middleware#ClearStackValues
|
||||||
|
// to clear all stack values.
|
||||||
|
func GetPayloadHash(ctx context.Context) (v string) {
|
||||||
|
v, _ = middleware.GetStackValue(ctx, payloadHashKey{}).(string)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPayloadHash sets the payload hash to be used for signing the request
|
||||||
|
//
|
||||||
|
// Scoped to stack values. Use github.com/aws/smithy-go/middleware#ClearStackValues
|
||||||
|
// to clear all stack values.
|
||||||
|
func SetPayloadHash(ctx context.Context, hash string) context.Context {
|
||||||
|
return middleware.WithStackValue(ctx, payloadHashKey{}, hash)
|
||||||
|
}
|
126
api/auth/signer/v4sdk2/signer/v4/presign_middleware.go
Normal file
126
api/auth/signer/v4sdk2/signer/v4/presign_middleware.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
||||||
|
"github.com/aws/smithy-go/middleware"
|
||||||
|
smithyHTTP "github.com/aws/smithy-go/transport/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPPresigner is an interface to a SigV4 signer that can sign create a
|
||||||
|
// presigned URL for a HTTP requests.
|
||||||
|
type HTTPPresigner interface {
|
||||||
|
PresignHTTP(
|
||||||
|
ctx context.Context, credentials aws.Credentials, r *http.Request,
|
||||||
|
payloadHash string, service string, region string, signingTime time.Time,
|
||||||
|
optFns ...func(*SignerOptions),
|
||||||
|
) (url string, signedHeader http.Header, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PresignedHTTPRequest provides the URL and signed headers that are included
|
||||||
|
// in the presigned URL.
|
||||||
|
type PresignedHTTPRequest struct {
|
||||||
|
URL string
|
||||||
|
Method string
|
||||||
|
SignedHeader http.Header
|
||||||
|
}
|
||||||
|
|
||||||
|
// PresignHTTPRequestMiddlewareOptions is the options for the PresignHTTPRequestMiddleware middleware.
|
||||||
|
type PresignHTTPRequestMiddlewareOptions struct {
|
||||||
|
CredentialsProvider aws.CredentialsProvider
|
||||||
|
Presigner HTTPPresigner
|
||||||
|
LogSigning bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// PresignHTTPRequestMiddleware provides the Finalize middleware for creating a
|
||||||
|
// presigned URL for an HTTP request.
|
||||||
|
//
|
||||||
|
// Will short circuit the middleware stack and not forward onto the next
|
||||||
|
// Finalize handler.
|
||||||
|
type PresignHTTPRequestMiddleware struct {
|
||||||
|
credentialsProvider aws.CredentialsProvider
|
||||||
|
presigner HTTPPresigner
|
||||||
|
logSigning bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPresignHTTPRequestMiddleware returns a new PresignHTTPRequestMiddleware
|
||||||
|
// initialized with the presigner.
|
||||||
|
func NewPresignHTTPRequestMiddleware(options PresignHTTPRequestMiddlewareOptions) *PresignHTTPRequestMiddleware {
|
||||||
|
return &PresignHTTPRequestMiddleware{
|
||||||
|
credentialsProvider: options.CredentialsProvider,
|
||||||
|
presigner: options.Presigner,
|
||||||
|
logSigning: options.LogSigning,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID provides the middleware ID.
|
||||||
|
func (*PresignHTTPRequestMiddleware) ID() string { return "PresignHTTPRequest" }
|
||||||
|
|
||||||
|
// HandleFinalize will take the provided input and create a presigned url for
|
||||||
|
// the http request using the SigV4 presign authentication scheme.
|
||||||
|
//
|
||||||
|
// Since the signed request is not a valid HTTP request
|
||||||
|
func (s *PresignHTTPRequestMiddleware) HandleFinalize(
|
||||||
|
ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler,
|
||||||
|
) (
|
||||||
|
out middleware.FinalizeOutput, metadata middleware.Metadata, err error,
|
||||||
|
) {
|
||||||
|
req, ok := in.Request.(*smithyHTTP.Request)
|
||||||
|
if !ok {
|
||||||
|
return out, metadata, &SigningError{
|
||||||
|
Err: fmt.Errorf("unexpected request middleware type %T", in.Request),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReq := req.Build(ctx)
|
||||||
|
if !haveCredentialProvider(s.credentialsProvider) {
|
||||||
|
out.Result = &PresignedHTTPRequest{
|
||||||
|
URL: httpReq.URL.String(),
|
||||||
|
Method: httpReq.Method,
|
||||||
|
SignedHeader: http.Header{},
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, metadata, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
signingName := awsmiddleware.GetSigningName(ctx)
|
||||||
|
signingRegion := awsmiddleware.GetSigningRegion(ctx)
|
||||||
|
payloadHash := GetPayloadHash(ctx)
|
||||||
|
if len(payloadHash) == 0 {
|
||||||
|
return out, metadata, &SigningError{
|
||||||
|
Err: fmt.Errorf("computed payload hash missing from context"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials, err := s.credentialsProvider.Retrieve(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return out, metadata, &SigningError{
|
||||||
|
Err: fmt.Errorf("failed to retrieve credentials: %w", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
u, h, err := s.presigner.PresignHTTP(ctx, credentials,
|
||||||
|
httpReq, payloadHash, signingName, signingRegion, time.Now(),
|
||||||
|
func(o *SignerOptions) {
|
||||||
|
o.Logger = middleware.GetLogger(ctx)
|
||||||
|
o.LogSigning = s.logSigning
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return out, metadata, &SigningError{
|
||||||
|
Err: fmt.Errorf("failed to sign http request, %w", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.Result = &PresignedHTTPRequest{
|
||||||
|
URL: u,
|
||||||
|
Method: httpReq.Method,
|
||||||
|
SignedHeader: h,
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, metadata, nil
|
||||||
|
}
|
87
api/auth/signer/v4sdk2/signer/v4/stream.go
Normal file
87
api/auth/signer/v4sdk2/signer/v4/stream.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
v4Internal "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/internal/v4"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EventStreamSigner is an AWS EventStream protocol signer.
|
||||||
|
type EventStreamSigner interface {
|
||||||
|
GetSignature(ctx context.Context, headers, payload []byte, signingTime time.Time, optFns ...func(*StreamSignerOptions)) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamSignerOptions is the configuration options for StreamSigner.
|
||||||
|
type StreamSignerOptions struct{}
|
||||||
|
|
||||||
|
// StreamSigner implements Signature Version 4 (SigV4) signing of event stream encoded payloads.
|
||||||
|
type StreamSigner struct {
|
||||||
|
options StreamSignerOptions
|
||||||
|
|
||||||
|
credentials aws.Credentials
|
||||||
|
service string
|
||||||
|
region string
|
||||||
|
|
||||||
|
prevSignature []byte
|
||||||
|
|
||||||
|
signingKeyDeriver *v4Internal.SigningKeyDeriver
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStreamSigner returns a new AWS EventStream protocol signer.
|
||||||
|
func NewStreamSigner(credentials aws.Credentials, service, region string, seedSignature []byte, optFns ...func(*StreamSignerOptions)) *StreamSigner {
|
||||||
|
o := StreamSignerOptions{}
|
||||||
|
|
||||||
|
for _, fn := range optFns {
|
||||||
|
fn(&o)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &StreamSigner{
|
||||||
|
options: o,
|
||||||
|
credentials: credentials,
|
||||||
|
service: service,
|
||||||
|
region: region,
|
||||||
|
signingKeyDeriver: v4Internal.NewSigningKeyDeriver(),
|
||||||
|
prevSignature: seedSignature,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSignature signs the provided header and payload bytes.
|
||||||
|
func (s *StreamSigner) GetSignature(ctx context.Context, headers, payload []byte, signingTime time.Time, optFns ...func(*StreamSignerOptions)) ([]byte, error) {
|
||||||
|
options := s.options
|
||||||
|
|
||||||
|
for _, fn := range optFns {
|
||||||
|
fn(&options)
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSignature := s.prevSignature
|
||||||
|
|
||||||
|
st := v4Internal.NewSigningTime(signingTime)
|
||||||
|
|
||||||
|
sigKey := s.signingKeyDeriver.DeriveKey(s.credentials, s.service, s.region, st)
|
||||||
|
|
||||||
|
scope := v4Internal.BuildCredentialScope(st, s.region, s.service)
|
||||||
|
|
||||||
|
stringToSign := s.buildEventStreamStringToSign(headers, payload, prevSignature, scope, &st)
|
||||||
|
|
||||||
|
signature := v4Internal.HMACSHA256(sigKey, []byte(stringToSign))
|
||||||
|
s.prevSignature = signature
|
||||||
|
|
||||||
|
return signature, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StreamSigner) buildEventStreamStringToSign(headers, payload, previousSignature []byte, credentialScope string, signingTime *v4Internal.SigningTime) string {
|
||||||
|
hash := sha256.New()
|
||||||
|
return strings.Join([]string{
|
||||||
|
"AWS4-HMAC-SHA256-PAYLOAD",
|
||||||
|
signingTime.TimeFormat(),
|
||||||
|
credentialScope,
|
||||||
|
hex.EncodeToString(previousSignature),
|
||||||
|
hex.EncodeToString(makeHash(hash, headers)),
|
||||||
|
hex.EncodeToString(makeHash(hash, payload)),
|
||||||
|
}, "\n")
|
||||||
|
}
|
570
api/auth/signer/v4sdk2/signer/v4/v4.go
Normal file
570
api/auth/signer/v4sdk2/signer/v4/v4.go
Normal file
|
@ -0,0 +1,570 @@
|
||||||
|
// Package v4 implements signing for AWS V4 signer
|
||||||
|
//
|
||||||
|
// Provides request signing for request that need to be signed with
|
||||||
|
// AWS V4 Signatures.
|
||||||
|
//
|
||||||
|
// # Standalone Signer
|
||||||
|
//
|
||||||
|
// Generally using the signer outside of the SDK should not require any additional
|
||||||
|
//
|
||||||
|
// The signer does this by taking advantage of the URL.EscapedPath method. If your request URI requires
|
||||||
|
//
|
||||||
|
// additional escaping you many need to use the URL.Opaque to define what the raw URI should be sent
|
||||||
|
// to the service as.
|
||||||
|
//
|
||||||
|
// The signer will first check the URL.Opaque field, and use its value if set.
|
||||||
|
// The signer does require the URL.Opaque field to be set in the form of:
|
||||||
|
//
|
||||||
|
// "//<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.
|
||||||
|
//
|
||||||
|
// AWS v4 signature validation requires that the canonical string's URI path
|
||||||
|
// element must be the URI escaped form of the HTTP request's path.
|
||||||
|
// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
||||||
|
//
|
||||||
|
// The Go HTTP client will perform escaping automatically on the request. Some
|
||||||
|
// of these escaping may cause signature validation errors because the HTTP
|
||||||
|
// request differs from the URI path or query that the signature was generated.
|
||||||
|
// https://golang.org/pkg/net/url/#URL.EscapedPath
|
||||||
|
//
|
||||||
|
// Because of this, it is recommended that when using the signer outside of the
|
||||||
|
// SDK that explicitly escaping the request prior to being signed is preferable,
|
||||||
|
// and will help prevent signature validation errors. This can be done by setting
|
||||||
|
// the URL.Opaque or URL.RawPath. The SDK will use URL.Opaque first and then
|
||||||
|
// call URL.EscapedPath() if Opaque is not set.
|
||||||
|
//
|
||||||
|
// Test `TestStandaloneSign` provides a complete example of using the signer
|
||||||
|
// outside of the SDK and pre-escaping the URI path.
|
||||||
|
package v4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
|
"net/url"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
v4Internal "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/internal/v4"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
"github.com/aws/smithy-go/encoding/httpbinding"
|
||||||
|
"github.com/aws/smithy-go/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
signingAlgorithm = "AWS4-HMAC-SHA256"
|
||||||
|
authorizationHeader = "Authorization"
|
||||||
|
|
||||||
|
// Version of signing v4
|
||||||
|
Version = "SigV4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPSigner is an interface to a SigV4 signer that can sign HTTP requests
|
||||||
|
type HTTPSigner interface {
|
||||||
|
SignHTTP(ctx context.Context, credentials aws.Credentials, r *http.Request, payloadHash string, service string, region string, signingTime time.Time, optFns ...func(*SignerOptions)) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyDerivator interface {
|
||||||
|
DeriveKey(credential aws.Credentials, service, region string, signingTime v4Internal.SigningTime) []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignerOptions is the SigV4 Signer options.
|
||||||
|
type SignerOptions struct {
|
||||||
|
// Disables the Signer's moving HTTP header key/value pairs from the HTTP
|
||||||
|
// request header to the request's query string. This is most commonly used
|
||||||
|
// with pre-signed requests preventing headers from being added to the
|
||||||
|
// request's query string.
|
||||||
|
DisableHeaderHoisting bool
|
||||||
|
|
||||||
|
// Disables the automatic escaping of the URI path of the request for the
|
||||||
|
// siganture's canonical string's path. For services that do not need additional
|
||||||
|
// escaping then use this to disable the signer escaping the path.
|
||||||
|
//
|
||||||
|
// S3 is an example of a service that does not need additional escaping.
|
||||||
|
//
|
||||||
|
// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
||||||
|
DisableURIPathEscaping bool
|
||||||
|
|
||||||
|
// The logger to send log messages to.
|
||||||
|
Logger logging.Logger
|
||||||
|
|
||||||
|
// Enable logging of signed requests.
|
||||||
|
// This will enable logging of the canonical request, the string to sign, and for presigning the subsequent
|
||||||
|
// presigned URL.
|
||||||
|
LogSigning bool
|
||||||
|
|
||||||
|
// Disables setting the session token on the request as part of signing
|
||||||
|
// through X-Amz-Security-Token. This is needed for variations of v4 that
|
||||||
|
// present the token elsewhere.
|
||||||
|
DisableSessionToken bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signer applies AWS v4 signing to given request. Use this to sign requests
|
||||||
|
// that need to be signed with AWS V4 Signatures.
|
||||||
|
type Signer struct {
|
||||||
|
options SignerOptions
|
||||||
|
keyDerivator keyDerivator
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSigner returns a new SigV4 Signer
|
||||||
|
func NewSigner(optFns ...func(signer *SignerOptions)) *Signer {
|
||||||
|
options := SignerOptions{}
|
||||||
|
|
||||||
|
for _, fn := range optFns {
|
||||||
|
fn(&options)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Signer{options: options, keyDerivator: v4Internal.NewSigningKeyDeriver()}
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpSigner struct {
|
||||||
|
Request *http.Request
|
||||||
|
ServiceName string
|
||||||
|
Region string
|
||||||
|
Time v4Internal.SigningTime
|
||||||
|
Credentials aws.Credentials
|
||||||
|
KeyDerivator keyDerivator
|
||||||
|
IsPreSign bool
|
||||||
|
|
||||||
|
PayloadHash string
|
||||||
|
|
||||||
|
DisableHeaderHoisting bool
|
||||||
|
DisableURIPathEscaping bool
|
||||||
|
DisableSessionToken bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpSigner) Build() (signedRequest, error) {
|
||||||
|
req := s.Request
|
||||||
|
|
||||||
|
query := req.URL.Query()
|
||||||
|
headers := req.Header
|
||||||
|
|
||||||
|
s.setRequiredSigningFields(headers, query)
|
||||||
|
|
||||||
|
// Sort Each Query Key's Values
|
||||||
|
for key := range query {
|
||||||
|
sort.Strings(query[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
v4Internal.SanitizeHostForHeader(req)
|
||||||
|
|
||||||
|
credentialScope := s.buildCredentialScope()
|
||||||
|
credentialStr := s.Credentials.AccessKeyID + "/" + credentialScope
|
||||||
|
if s.IsPreSign {
|
||||||
|
query.Set(v4Internal.AmzCredentialKey, credentialStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
unsignedHeaders := headers
|
||||||
|
if s.IsPreSign && !s.DisableHeaderHoisting {
|
||||||
|
var urlValues url.Values
|
||||||
|
urlValues, unsignedHeaders = buildQuery(v4Internal.AllowedQueryHoisting, headers)
|
||||||
|
for k := range urlValues {
|
||||||
|
query[k] = urlValues[k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
host := req.URL.Host
|
||||||
|
if len(req.Host) > 0 {
|
||||||
|
host = req.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
signedHeaders http.Header
|
||||||
|
signedHeadersStr string
|
||||||
|
canonicalHeaderStr string
|
||||||
|
)
|
||||||
|
|
||||||
|
if s.IsPreSign {
|
||||||
|
signedHeaders, signedHeadersStr, canonicalHeaderStr = s.buildCanonicalHeaders(host, v4Internal.IgnoredPresignedHeaders, unsignedHeaders, s.Request.ContentLength)
|
||||||
|
} else {
|
||||||
|
signedHeaders, signedHeadersStr, canonicalHeaderStr = s.buildCanonicalHeaders(host, v4Internal.IgnoredHeaders, unsignedHeaders, s.Request.ContentLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.IsPreSign {
|
||||||
|
query.Set(v4Internal.AmzSignedHeadersKey, signedHeadersStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rawQuery strings.Builder
|
||||||
|
rawQuery.WriteString(strings.Replace(query.Encode(), "+", "%20", -1))
|
||||||
|
|
||||||
|
canonicalURI := v4Internal.GetURIPath(req.URL)
|
||||||
|
if !s.DisableURIPathEscaping {
|
||||||
|
canonicalURI = httpbinding.EscapePath(canonicalURI, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
canonicalString := s.buildCanonicalString(
|
||||||
|
req.Method,
|
||||||
|
canonicalURI,
|
||||||
|
rawQuery.String(),
|
||||||
|
signedHeadersStr,
|
||||||
|
canonicalHeaderStr,
|
||||||
|
)
|
||||||
|
|
||||||
|
strToSign := s.buildStringToSign(credentialScope, canonicalString)
|
||||||
|
signingSignature, err := s.buildSignature(strToSign)
|
||||||
|
if err != nil {
|
||||||
|
return signedRequest{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.IsPreSign {
|
||||||
|
rawQuery.WriteString("&X-Amz-Signature=")
|
||||||
|
rawQuery.WriteString(signingSignature)
|
||||||
|
} else {
|
||||||
|
headers[authorizationHeader] = append(headers[authorizationHeader][:0], buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature))
|
||||||
|
}
|
||||||
|
|
||||||
|
req.URL.RawQuery = rawQuery.String()
|
||||||
|
|
||||||
|
return signedRequest{
|
||||||
|
Request: req,
|
||||||
|
SignedHeaders: signedHeaders,
|
||||||
|
CanonicalString: canonicalString,
|
||||||
|
StringToSign: strToSign,
|
||||||
|
PreSigned: s.IsPreSign,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature string) string {
|
||||||
|
const credential = "Credential="
|
||||||
|
const signedHeaders = "SignedHeaders="
|
||||||
|
const signature = "Signature="
|
||||||
|
const commaSpace = ", "
|
||||||
|
|
||||||
|
var parts strings.Builder
|
||||||
|
parts.Grow(len(signingAlgorithm) + 1 +
|
||||||
|
len(credential) + len(credentialStr) + 2 +
|
||||||
|
len(signedHeaders) + len(signedHeadersStr) + 2 +
|
||||||
|
len(signature) + len(signingSignature),
|
||||||
|
)
|
||||||
|
parts.WriteString(signingAlgorithm)
|
||||||
|
parts.WriteRune(' ')
|
||||||
|
parts.WriteString(credential)
|
||||||
|
parts.WriteString(credentialStr)
|
||||||
|
parts.WriteString(commaSpace)
|
||||||
|
parts.WriteString(signedHeaders)
|
||||||
|
parts.WriteString(signedHeadersStr)
|
||||||
|
parts.WriteString(commaSpace)
|
||||||
|
parts.WriteString(signature)
|
||||||
|
parts.WriteString(signingSignature)
|
||||||
|
return parts.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignHTTP signs AWS v4 requests with the provided payload hash, service name, region the
|
||||||
|
// request is made to, and time the request is signed at. The signTime allows
|
||||||
|
// you to specify that a request is signed for the future, and cannot be
|
||||||
|
// used until then.
|
||||||
|
//
|
||||||
|
// The payloadHash is the hex encoded SHA-256 hash of the request payload, and
|
||||||
|
// must be provided. Even if the request has no payload (aka body). If the
|
||||||
|
// request has no payload you should use the hex encoded SHA-256 of an empty
|
||||||
|
// string as the payloadHash value.
|
||||||
|
//
|
||||||
|
// "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
//
|
||||||
|
// Some services such as Amazon S3 accept alternative values for the payload
|
||||||
|
// hash, such as "UNSIGNED-PAYLOAD" for requests where the body will not be
|
||||||
|
// included in the request signature.
|
||||||
|
//
|
||||||
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
|
||||||
|
//
|
||||||
|
// Sign differs from Presign in that it will sign the request using HTTP
|
||||||
|
// header values. This type of signing is intended for http.Request values that
|
||||||
|
// will not be shared, or are shared in a way the header values on the request
|
||||||
|
// will not be lost.
|
||||||
|
//
|
||||||
|
// The passed in request will be modified in place.
|
||||||
|
func (s Signer) SignHTTP(ctx context.Context, credentials aws.Credentials, r *http.Request, payloadHash string, service string, region string, signingTime time.Time, optFns ...func(options *SignerOptions)) error {
|
||||||
|
options := s.options
|
||||||
|
|
||||||
|
for _, fn := range optFns {
|
||||||
|
fn(&options)
|
||||||
|
}
|
||||||
|
|
||||||
|
signer := &httpSigner{
|
||||||
|
Request: r,
|
||||||
|
PayloadHash: payloadHash,
|
||||||
|
ServiceName: service,
|
||||||
|
Region: region,
|
||||||
|
Credentials: credentials,
|
||||||
|
Time: v4Internal.NewSigningTime(signingTime.UTC()),
|
||||||
|
DisableHeaderHoisting: options.DisableHeaderHoisting,
|
||||||
|
DisableURIPathEscaping: options.DisableURIPathEscaping,
|
||||||
|
DisableSessionToken: options.DisableSessionToken,
|
||||||
|
KeyDerivator: s.keyDerivator,
|
||||||
|
}
|
||||||
|
|
||||||
|
signedRequest, err := signer.Build()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logSigningInfo(ctx, options, &signedRequest, false)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PresignHTTP signs AWS v4 requests with the payload hash, service name, region
|
||||||
|
// the request is made to, and time the request is signed at. The signTime
|
||||||
|
// allows you to specify that a request is signed for the future, and cannot
|
||||||
|
// be used until then.
|
||||||
|
//
|
||||||
|
// Returns the signed URL and the map of HTTP headers that were included in the
|
||||||
|
// signature or an error if signing the request failed. For presigned requests
|
||||||
|
// these headers and their values must be included on the HTTP request when it
|
||||||
|
// is made. This is helpful to know what header values need to be shared with
|
||||||
|
// the party the presigned request will be distributed to.
|
||||||
|
//
|
||||||
|
// The payloadHash is the hex encoded SHA-256 hash of the request payload, and
|
||||||
|
// must be provided. Even if the request has no payload (aka body). If the
|
||||||
|
// request has no payload you should use the hex encoded SHA-256 of an empty
|
||||||
|
// string as the payloadHash value.
|
||||||
|
//
|
||||||
|
// "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
//
|
||||||
|
// Some services such as Amazon S3 accept alternative values for the payload
|
||||||
|
// hash, such as "UNSIGNED-PAYLOAD" for requests where the body will not be
|
||||||
|
// included in the request signature.
|
||||||
|
//
|
||||||
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
|
||||||
|
//
|
||||||
|
// PresignHTTP differs from SignHTTP in that it will sign the request using
|
||||||
|
// query string instead of header values. This allows you to share the
|
||||||
|
// Presigned Request's URL with third parties, or distribute it throughout your
|
||||||
|
// system with minimal dependencies.
|
||||||
|
//
|
||||||
|
// PresignHTTP will not set the expires time of the presigned request
|
||||||
|
// automatically. To specify the expire duration for a request add the
|
||||||
|
// "X-Amz-Expires" query parameter on the request with the value as the
|
||||||
|
// duration in seconds the presigned URL should be considered valid for. This
|
||||||
|
// parameter is not used by all AWS services, and is most notable used by
|
||||||
|
// Amazon S3 APIs.
|
||||||
|
//
|
||||||
|
// expires := 20 * time.Minute
|
||||||
|
// query := req.URL.Query()
|
||||||
|
// query.Set("X-Amz-Expires", strconv.FormatInt(int64(expires/time.Second), 10))
|
||||||
|
// req.URL.RawQuery = query.Encode()
|
||||||
|
//
|
||||||
|
// This method does not modify the provided request.
|
||||||
|
func (s *Signer) PresignHTTP(
|
||||||
|
ctx context.Context, credentials aws.Credentials, r *http.Request,
|
||||||
|
payloadHash string, service string, region string, signingTime time.Time,
|
||||||
|
optFns ...func(*SignerOptions),
|
||||||
|
) (signedURI string, signedHeaders http.Header, err error) {
|
||||||
|
options := s.options
|
||||||
|
|
||||||
|
for _, fn := range optFns {
|
||||||
|
fn(&options)
|
||||||
|
}
|
||||||
|
|
||||||
|
signer := &httpSigner{
|
||||||
|
Request: r.Clone(r.Context()),
|
||||||
|
PayloadHash: payloadHash,
|
||||||
|
ServiceName: service,
|
||||||
|
Region: region,
|
||||||
|
Credentials: credentials,
|
||||||
|
Time: v4Internal.NewSigningTime(signingTime.UTC()),
|
||||||
|
IsPreSign: true,
|
||||||
|
DisableHeaderHoisting: options.DisableHeaderHoisting,
|
||||||
|
DisableURIPathEscaping: options.DisableURIPathEscaping,
|
||||||
|
DisableSessionToken: options.DisableSessionToken,
|
||||||
|
KeyDerivator: s.keyDerivator,
|
||||||
|
}
|
||||||
|
|
||||||
|
signedRequest, err := signer.Build()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logSigningInfo(ctx, options, &signedRequest, true)
|
||||||
|
|
||||||
|
signedHeaders = make(http.Header)
|
||||||
|
|
||||||
|
// For the signed headers we canonicalize the header keys in the returned map.
|
||||||
|
// This avoids situations where can standard library double headers like host header. For example the standard
|
||||||
|
// library will set the Host header, even if it is present in lower-case form.
|
||||||
|
for k, v := range signedRequest.SignedHeaders {
|
||||||
|
key := textproto.CanonicalMIMEHeaderKey(k)
|
||||||
|
signedHeaders[key] = append(signedHeaders[key], v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return signedRequest.Request.URL.String(), signedHeaders, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpSigner) buildCredentialScope() string {
|
||||||
|
return v4Internal.BuildCredentialScope(s.Time, s.Region, s.ServiceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildQuery(r v4Internal.Rule, header http.Header) (url.Values, http.Header) {
|
||||||
|
query := url.Values{}
|
||||||
|
unsignedHeaders := http.Header{}
|
||||||
|
for k, h := range header {
|
||||||
|
if r.IsValid(k) {
|
||||||
|
query[k] = h
|
||||||
|
} else {
|
||||||
|
unsignedHeaders[k] = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query, unsignedHeaders
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpSigner) buildCanonicalHeaders(host string, rule v4Internal.Rule, header http.Header, length int64) (signed http.Header, signedHeaders, canonicalHeadersStr string) {
|
||||||
|
signed = make(http.Header)
|
||||||
|
|
||||||
|
var headers []string
|
||||||
|
const hostHeader = "host"
|
||||||
|
headers = append(headers, hostHeader)
|
||||||
|
signed[hostHeader] = append(signed[hostHeader], host)
|
||||||
|
|
||||||
|
const contentLengthHeader = "content-length"
|
||||||
|
if length > 0 {
|
||||||
|
headers = append(headers, contentLengthHeader)
|
||||||
|
signed[contentLengthHeader] = append(signed[contentLengthHeader], strconv.FormatInt(length, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range header {
|
||||||
|
if !rule.IsValid(k) {
|
||||||
|
continue // ignored header
|
||||||
|
}
|
||||||
|
if strings.EqualFold(k, contentLengthHeader) {
|
||||||
|
// prevent signing already handled content-length header.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lowerCaseKey := strings.ToLower(k)
|
||||||
|
if _, ok := signed[lowerCaseKey]; ok {
|
||||||
|
// include additional values
|
||||||
|
signed[lowerCaseKey] = append(signed[lowerCaseKey], v...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = append(headers, lowerCaseKey)
|
||||||
|
signed[lowerCaseKey] = v
|
||||||
|
}
|
||||||
|
sort.Strings(headers)
|
||||||
|
|
||||||
|
signedHeaders = strings.Join(headers, ";")
|
||||||
|
|
||||||
|
var canonicalHeaders strings.Builder
|
||||||
|
n := len(headers)
|
||||||
|
const colon = ':'
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if headers[i] == hostHeader {
|
||||||
|
canonicalHeaders.WriteString(hostHeader)
|
||||||
|
canonicalHeaders.WriteRune(colon)
|
||||||
|
canonicalHeaders.WriteString(v4Internal.StripExcessSpaces(host))
|
||||||
|
} else {
|
||||||
|
canonicalHeaders.WriteString(headers[i])
|
||||||
|
canonicalHeaders.WriteRune(colon)
|
||||||
|
// Trim out leading, trailing, and dedup inner spaces from signed header values.
|
||||||
|
values := signed[headers[i]]
|
||||||
|
for j, v := range values {
|
||||||
|
cleanedValue := strings.TrimSpace(v4Internal.StripExcessSpaces(v))
|
||||||
|
canonicalHeaders.WriteString(cleanedValue)
|
||||||
|
if j < len(values)-1 {
|
||||||
|
canonicalHeaders.WriteRune(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canonicalHeaders.WriteRune('\n')
|
||||||
|
}
|
||||||
|
canonicalHeadersStr = canonicalHeaders.String()
|
||||||
|
|
||||||
|
return signed, signedHeaders, canonicalHeadersStr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpSigner) buildCanonicalString(method, uri, query, signedHeaders, canonicalHeaders string) string {
|
||||||
|
return strings.Join([]string{
|
||||||
|
method,
|
||||||
|
uri,
|
||||||
|
query,
|
||||||
|
canonicalHeaders,
|
||||||
|
signedHeaders,
|
||||||
|
s.PayloadHash,
|
||||||
|
}, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpSigner) buildStringToSign(credentialScope, canonicalRequestString string) string {
|
||||||
|
return strings.Join([]string{
|
||||||
|
signingAlgorithm,
|
||||||
|
s.Time.TimeFormat(),
|
||||||
|
credentialScope,
|
||||||
|
hex.EncodeToString(makeHash(sha256.New(), []byte(canonicalRequestString))),
|
||||||
|
}, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeHash(hash hash.Hash, b []byte) []byte {
|
||||||
|
hash.Reset()
|
||||||
|
hash.Write(b)
|
||||||
|
return hash.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpSigner) buildSignature(strToSign string) (string, error) {
|
||||||
|
key := s.KeyDerivator.DeriveKey(s.Credentials, s.ServiceName, s.Region, s.Time)
|
||||||
|
return hex.EncodeToString(v4Internal.HMACSHA256(key, []byte(strToSign))), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpSigner) setRequiredSigningFields(headers http.Header, query url.Values) {
|
||||||
|
amzDate := s.Time.TimeFormat()
|
||||||
|
|
||||||
|
if s.IsPreSign {
|
||||||
|
query.Set(v4Internal.AmzAlgorithmKey, signingAlgorithm)
|
||||||
|
sessionToken := s.Credentials.SessionToken
|
||||||
|
if !s.DisableSessionToken && len(sessionToken) > 0 {
|
||||||
|
query.Set("X-Amz-Security-Token", sessionToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Set(v4Internal.AmzDateKey, amzDate)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
headers[v4Internal.AmzDateKey] = append(headers[v4Internal.AmzDateKey][:0], amzDate)
|
||||||
|
|
||||||
|
if !s.DisableSessionToken && len(s.Credentials.SessionToken) > 0 {
|
||||||
|
headers[v4Internal.AmzSecurityTokenKey] = append(headers[v4Internal.AmzSecurityTokenKey][:0], s.Credentials.SessionToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func logSigningInfo(ctx context.Context, options SignerOptions, request *signedRequest, isPresign bool) {
|
||||||
|
if !options.LogSigning {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
signedURLMsg := ""
|
||||||
|
if isPresign {
|
||||||
|
signedURLMsg = fmt.Sprintf(logSignedURLMsg, request.Request.URL.String())
|
||||||
|
}
|
||||||
|
logger := logging.WithContext(ctx, options.Logger)
|
||||||
|
logger.Logf(logging.Debug, logSignInfoMsg, request.CanonicalString, request.StringToSign, signedURLMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
type signedRequest struct {
|
||||||
|
Request *http.Request
|
||||||
|
SignedHeaders http.Header
|
||||||
|
CanonicalString string
|
||||||
|
StringToSign string
|
||||||
|
PreSigned bool
|
||||||
|
}
|
||||||
|
|
||||||
|
const logSignInfoMsg = `Request Signature:
|
||||||
|
---[ CANONICAL STRING ]-----------------------------
|
||||||
|
%s
|
||||||
|
---[ STRING TO SIGN ]--------------------------------
|
||||||
|
%s%s
|
||||||
|
-----------------------------------------------------`
|
||||||
|
const logSignedURLMsg = `
|
||||||
|
---[ SIGNED URL ]------------------------------------
|
||||||
|
%s`
|
363
api/auth/signer/v4sdk2/signer/v4/v4_test.go
Normal file
363
api/auth/signer/v4sdk2/signer/v4/v4_test.go
Normal file
|
@ -0,0 +1,363 @@
|
||||||
|
package v4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
v4Internal "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/internal/v4"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testCredentials = aws.Credentials{AccessKeyID: "AKID", SecretAccessKey: "SECRET", SessionToken: "SESSION"}
|
||||||
|
|
||||||
|
func buildRequest(serviceName, region, body string) (*http.Request, string) {
|
||||||
|
reader := strings.NewReader(body)
|
||||||
|
return buildRequestWithBodyReader(serviceName, region, reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildRequestWithBodyReader(serviceName, region string, body io.Reader) (*http.Request, string) {
|
||||||
|
var bodyLen int
|
||||||
|
|
||||||
|
type lenner interface {
|
||||||
|
Len() int
|
||||||
|
}
|
||||||
|
if lr, ok := body.(lenner); ok {
|
||||||
|
bodyLen = lr.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint := "https://" + serviceName + "." + region + ".amazonaws.com"
|
||||||
|
req, _ := http.NewRequest("POST", endpoint, body)
|
||||||
|
req.URL.Opaque = "//example.org/bucket/key-._~,!@#$%^&*()"
|
||||||
|
req.Header.Set("X-Amz-Target", "prefix.Operation")
|
||||||
|
req.Header.Set("Content-Type", "application/x-amz-json-1.0")
|
||||||
|
|
||||||
|
if bodyLen > 0 {
|
||||||
|
req.ContentLength = int64(bodyLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("X-Amz-Meta-Other-Header", "some-value=!@#$%^&* (+)")
|
||||||
|
req.Header.Add("X-Amz-Meta-Other-Header_With_Underscore", "some-value=!@#$%^&* (+)")
|
||||||
|
req.Header.Add("X-amz-Meta-Other-Header_With_Underscore", "some-value=!@#$%^&* (+)")
|
||||||
|
|
||||||
|
h := sha256.New()
|
||||||
|
_, _ = io.Copy(h, body)
|
||||||
|
payloadHash := hex.EncodeToString(h.Sum(nil))
|
||||||
|
|
||||||
|
return req, payloadHash
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPresignRequest(t *testing.T) {
|
||||||
|
req, body := buildRequest("dynamodb", "us-east-1", "{}")
|
||||||
|
|
||||||
|
query := req.URL.Query()
|
||||||
|
query.Set("X-Amz-Expires", "300")
|
||||||
|
req.URL.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
signer := NewSigner()
|
||||||
|
signed, headers, err := signer.PresignHTTP(context.Background(), testCredentials, req, body, "dynamodb", "us-east-1", time.Unix(0, 0))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedDate := "19700101T000000Z"
|
||||||
|
expectedHeaders := "content-length;content-type;host;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore"
|
||||||
|
expectedSig := "122f0b9e091e4ba84286097e2b3404a1f1f4c4aad479adda95b7dff0ccbe5581"
|
||||||
|
expectedCred := "AKID/19700101/us-east-1/dynamodb/aws4_request"
|
||||||
|
expectedTarget := "prefix.Operation"
|
||||||
|
|
||||||
|
q, err := url.ParseQuery(signed[strings.Index(signed, "?"):])
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expect no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, a := expectedSig, q.Get("X-Amz-Signature"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := expectedCred, q.Get("X-Amz-Credential"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := expectedHeaders, q.Get("X-Amz-SignedHeaders"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := expectedDate, q.Get("X-Amz-Date"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if a := q.Get("X-Amz-Meta-Other-Header"); len(a) != 0 {
|
||||||
|
t.Errorf("expect %v to be empty", a)
|
||||||
|
}
|
||||||
|
if e, a := expectedTarget, q.Get("X-Amz-Target"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, h := range strings.Split(expectedHeaders, ";") {
|
||||||
|
v := headers.Get(h)
|
||||||
|
if len(v) == 0 {
|
||||||
|
t.Errorf("expect %v, to be present in header map", h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPresignBodyWithArrayRequest(t *testing.T) {
|
||||||
|
req, body := buildRequest("dynamodb", "us-east-1", "{}")
|
||||||
|
req.URL.RawQuery = "Foo=z&Foo=o&Foo=m&Foo=a"
|
||||||
|
|
||||||
|
query := req.URL.Query()
|
||||||
|
query.Set("X-Amz-Expires", "300")
|
||||||
|
req.URL.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
signer := NewSigner()
|
||||||
|
signed, headers, err := signer.PresignHTTP(context.Background(), testCredentials, req, body, "dynamodb", "us-east-1", time.Unix(0, 0))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expect no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
q, err := url.ParseQuery(signed[strings.Index(signed, "?"):])
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expect no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedDate := "19700101T000000Z"
|
||||||
|
expectedHeaders := "content-length;content-type;host;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore"
|
||||||
|
expectedSig := "e3ac55addee8711b76c6d608d762cff285fe8b627a057f8b5ec9268cf82c08b1"
|
||||||
|
expectedCred := "AKID/19700101/us-east-1/dynamodb/aws4_request"
|
||||||
|
expectedTarget := "prefix.Operation"
|
||||||
|
|
||||||
|
if e, a := expectedSig, q.Get("X-Amz-Signature"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := expectedCred, q.Get("X-Amz-Credential"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := expectedHeaders, q.Get("X-Amz-SignedHeaders"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := expectedDate, q.Get("X-Amz-Date"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if a := q.Get("X-Amz-Meta-Other-Header"); len(a) != 0 {
|
||||||
|
t.Errorf("expect %v to be empty, was not", a)
|
||||||
|
}
|
||||||
|
if e, a := expectedTarget, q.Get("X-Amz-Target"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, h := range strings.Split(expectedHeaders, ";") {
|
||||||
|
v := headers.Get(h)
|
||||||
|
if len(v) == 0 {
|
||||||
|
t.Errorf("expect %v, to be present in header map", h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignRequest(t *testing.T) {
|
||||||
|
req, body := buildRequest("dynamodb", "us-east-1", "{}")
|
||||||
|
signer := NewSigner()
|
||||||
|
err := signer.SignHTTP(context.Background(), testCredentials, req, body, "dynamodb", "us-east-1", time.Unix(0, 0))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expect no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedDate := "19700101T000000Z"
|
||||||
|
expectedSig := "AWS4-HMAC-SHA256 Credential=AKID/19700101/us-east-1/dynamodb/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore;x-amz-security-token;x-amz-target, Signature=a518299330494908a70222cec6899f6f32f297f8595f6df1776d998936652ad9"
|
||||||
|
|
||||||
|
q := req.Header
|
||||||
|
if e, a := expectedSig, q.Get("Authorization"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := expectedDate, q.Get("X-Amz-Date"); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildCanonicalRequest(t *testing.T) {
|
||||||
|
req, _ := buildRequest("dynamodb", "us-east-1", "{}")
|
||||||
|
req.URL.RawQuery = "Foo=z&Foo=o&Foo=m&Foo=a"
|
||||||
|
|
||||||
|
ctx := &httpSigner{
|
||||||
|
ServiceName: "dynamodb",
|
||||||
|
Region: "us-east-1",
|
||||||
|
Request: req,
|
||||||
|
Time: v4Internal.NewSigningTime(time.Now()),
|
||||||
|
KeyDerivator: v4Internal.NewSigningKeyDeriver(),
|
||||||
|
}
|
||||||
|
|
||||||
|
build, err := ctx.Build()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := "https://example.org/bucket/key-._~,!@#$%^&*()?Foo=a&Foo=m&Foo=o&Foo=z"
|
||||||
|
if e, a := expected, build.Request.URL.String(); e != a {
|
||||||
|
t.Errorf("expect %v, got %v", e, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSigner_SignHTTP_NoReplaceRequestBody(t *testing.T) {
|
||||||
|
req, bodyHash := buildRequest("dynamodb", "us-east-1", "{}")
|
||||||
|
req.Body = ioutil.NopCloser(bytes.NewReader([]byte{}))
|
||||||
|
|
||||||
|
s := NewSigner()
|
||||||
|
|
||||||
|
origBody := req.Body
|
||||||
|
|
||||||
|
err := s.SignHTTP(context.Background(), testCredentials, req, bodyHash, "dynamodb", "us-east-1", time.Now())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expect no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Body != origBody {
|
||||||
|
t.Errorf("expect request body to not be chagned")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestHost(t *testing.T) {
|
||||||
|
req, _ := buildRequest("dynamodb", "us-east-1", "{}")
|
||||||
|
req.URL.RawQuery = "Foo=z&Foo=o&Foo=m&Foo=a"
|
||||||
|
req.Host = "myhost"
|
||||||
|
|
||||||
|
query := req.URL.Query()
|
||||||
|
query.Set("X-Amz-Expires", "5")
|
||||||
|
req.URL.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
ctx := &httpSigner{
|
||||||
|
ServiceName: "dynamodb",
|
||||||
|
Region: "us-east-1",
|
||||||
|
Request: req,
|
||||||
|
Time: v4Internal.NewSigningTime(time.Now()),
|
||||||
|
KeyDerivator: v4Internal.NewSigningKeyDeriver(),
|
||||||
|
}
|
||||||
|
|
||||||
|
build, err := ctx.Build()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(build.CanonicalString, "host:"+req.Host) {
|
||||||
|
t.Errorf("canonical host header invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSign_buildCanonicalHeadersContentLengthPresent(t *testing.T) {
|
||||||
|
body := `{"description": "this is a test"}`
|
||||||
|
req, _ := buildRequest("dynamodb", "us-east-1", body)
|
||||||
|
req.URL.RawQuery = "Foo=z&Foo=o&Foo=m&Foo=a"
|
||||||
|
req.Host = "myhost"
|
||||||
|
|
||||||
|
contentLength := fmt.Sprintf("%d", len([]byte(body)))
|
||||||
|
req.Header.Add("Content-Length", contentLength)
|
||||||
|
|
||||||
|
query := req.URL.Query()
|
||||||
|
query.Set("X-Amz-Expires", "5")
|
||||||
|
req.URL.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
ctx := &httpSigner{
|
||||||
|
ServiceName: "dynamodb",
|
||||||
|
Region: "us-east-1",
|
||||||
|
Request: req,
|
||||||
|
Time: v4Internal.NewSigningTime(time.Now()),
|
||||||
|
KeyDerivator: v4Internal.NewSigningKeyDeriver(),
|
||||||
|
}
|
||||||
|
|
||||||
|
build, err := ctx.Build()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(build.CanonicalString, "content-length:"+contentLength+"\n") {
|
||||||
|
t.Errorf("canonical header content-length invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSign_buildCanonicalHeaders(t *testing.T) {
|
||||||
|
serviceName := "mockAPI"
|
||||||
|
region := "mock-region"
|
||||||
|
endpoint := "https://" + serviceName + "." + region + ".amazonaws.com"
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create request, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("FooInnerSpace", " inner space ")
|
||||||
|
req.Header.Set("FooLeadingSpace", " leading-space")
|
||||||
|
req.Header.Add("FooMultipleSpace", "no-space")
|
||||||
|
req.Header.Add("FooMultipleSpace", "\ttab-space")
|
||||||
|
req.Header.Add("FooMultipleSpace", "trailing-space ")
|
||||||
|
req.Header.Set("FooNoSpace", "no-space")
|
||||||
|
req.Header.Set("FooTabSpace", "\ttab-space\t")
|
||||||
|
req.Header.Set("FooTrailingSpace", "trailing-space ")
|
||||||
|
req.Header.Set("FooWrappedSpace", " wrapped-space ")
|
||||||
|
|
||||||
|
ctx := &httpSigner{
|
||||||
|
ServiceName: serviceName,
|
||||||
|
Region: region,
|
||||||
|
Request: req,
|
||||||
|
Time: v4Internal.NewSigningTime(time.Date(2021, 10, 20, 12, 42, 0, 0, time.UTC)),
|
||||||
|
KeyDerivator: v4Internal.NewSigningKeyDeriver(),
|
||||||
|
}
|
||||||
|
|
||||||
|
build, err := ctx.Build()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectCanonicalString := strings.Join([]string{
|
||||||
|
`POST`,
|
||||||
|
`/`,
|
||||||
|
``,
|
||||||
|
`fooinnerspace:inner space`,
|
||||||
|
`fooleadingspace:leading-space`,
|
||||||
|
`foomultiplespace:no-space,tab-space,trailing-space`,
|
||||||
|
`foonospace:no-space`,
|
||||||
|
`footabspace:tab-space`,
|
||||||
|
`footrailingspace:trailing-space`,
|
||||||
|
`foowrappedspace:wrapped-space`,
|
||||||
|
`host:mockAPI.mock-region.amazonaws.com`,
|
||||||
|
`x-amz-date:20211020T124200Z`,
|
||||||
|
``,
|
||||||
|
`fooinnerspace;fooleadingspace;foomultiplespace;foonospace;footabspace;footrailingspace;foowrappedspace;host;x-amz-date`,
|
||||||
|
``,
|
||||||
|
}, "\n")
|
||||||
|
if diff := cmpDiff(expectCanonicalString, build.CanonicalString); diff != "" {
|
||||||
|
t.Errorf("expect match, got\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPresignRequest(b *testing.B) {
|
||||||
|
signer := NewSigner()
|
||||||
|
req, bodyHash := buildRequest("dynamodb", "us-east-1", "{}")
|
||||||
|
|
||||||
|
query := req.URL.Query()
|
||||||
|
query.Set("X-Amz-Expires", "5")
|
||||||
|
req.URL.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
signer.PresignHTTP(context.Background(), testCredentials, req, bodyHash, "dynamodb", "us-east-1", time.Now())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkSignRequest(b *testing.B) {
|
||||||
|
signer := NewSigner()
|
||||||
|
req, bodyHash := buildRequest("dynamodb", "us-east-1", "{}")
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
signer.SignHTTP(context.Background(), testCredentials, req, bodyHash, "dynamodb", "us-east-1", time.Now())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmpDiff(e, a interface{}) string {
|
||||||
|
if !reflect.DeepEqual(e, a) {
|
||||||
|
return fmt.Sprintf("%v != %v", e, a)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
|
@ -312,7 +312,8 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handler) getBodyReader(r *http.Request) (io.ReadCloser, error) {
|
func (h *handler) getBodyReader(r *http.Request) (io.ReadCloser, error) {
|
||||||
if !api.IsSignedStreamingV4(r) {
|
shaType, streaming := api.IsSignedStreamingV4(r)
|
||||||
|
if !streaming {
|
||||||
return r.Body, nil
|
return r.Body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,7 +346,16 @@ func (h *handler) getBodyReader(r *http.Request) (io.ReadCloser, error) {
|
||||||
return nil, fmt.Errorf("%w: parse decoded content length: %s", apierr.GetAPIError(apierr.ErrMissingContentLength), err.Error())
|
return nil, fmt.Errorf("%w: parse decoded content length: %s", apierr.GetAPIError(apierr.ErrMissingContentLength), err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
chunkReader, err := newSignV4ChunkedReader(r)
|
var (
|
||||||
|
err error
|
||||||
|
chunkReader io.ReadCloser
|
||||||
|
)
|
||||||
|
if shaType == api.StreamingContentV4aSHA256 {
|
||||||
|
chunkReader, err = newSignV4aChunkedReader(r)
|
||||||
|
} else {
|
||||||
|
chunkReader, err = newSignV4ChunkedReader(r)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("initialize chunk reader: %w", err)
|
return nil, fmt.Errorf("initialize chunk reader: %w", err)
|
||||||
}
|
}
|
||||||
|
|
209
api/handler/s3v4aReader.go
Normal file
209
api/handler/s3v4aReader.go
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2"
|
||||||
|
errs "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
||||||
|
credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
s3v4aChunkReader struct {
|
||||||
|
reader *bufio.Reader
|
||||||
|
streamSigner *v4a.StreamSigner
|
||||||
|
|
||||||
|
requestTime time.Time
|
||||||
|
buffer []byte
|
||||||
|
offset int
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *s3v4aChunkReader) Close() (err error) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *s3v4aChunkReader) Read(buf []byte) (num int, err error) {
|
||||||
|
if c.offset > 0 {
|
||||||
|
num = copy(buf, c.buffer[c.offset:])
|
||||||
|
if num == len(buf) {
|
||||||
|
c.offset += num
|
||||||
|
return num, nil
|
||||||
|
}
|
||||||
|
c.offset = 0
|
||||||
|
buf = buf[num:]
|
||||||
|
}
|
||||||
|
|
||||||
|
var size int
|
||||||
|
for {
|
||||||
|
b, err := c.reader.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return c.handleErr(num, err)
|
||||||
|
}
|
||||||
|
if b == ';' { // separating character
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manually deserialize the size since AWS specified
|
||||||
|
// the chunk size to be of variable width. In particular,
|
||||||
|
// a size of 16 is encoded as `10` while a size of 64 KB
|
||||||
|
// is `10000`.
|
||||||
|
switch {
|
||||||
|
case b >= '0' && b <= '9':
|
||||||
|
size = size<<4 | int(b-'0')
|
||||||
|
case b >= 'a' && b <= 'f':
|
||||||
|
size = size<<4 | int(b-('a'-10))
|
||||||
|
case b >= 'A' && b <= 'F':
|
||||||
|
size = size<<4 | int(b-('A'-10))
|
||||||
|
default:
|
||||||
|
c.err = errMalformedChunkedEncoding
|
||||||
|
return num, c.err
|
||||||
|
}
|
||||||
|
if size > maxChunkSize {
|
||||||
|
c.err = errGiantChunk
|
||||||
|
return num, c.err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, we read the signature of the following payload and expect:
|
||||||
|
// chunk-signature=" + <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,7 +94,8 @@ const (
|
||||||
|
|
||||||
DefaultLocationConstraint = "default"
|
DefaultLocationConstraint = "default"
|
||||||
|
|
||||||
StreamingContentSHA256 = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
|
StreamingContentSHA256 = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
|
||||||
|
StreamingContentV4aSHA256 = "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD"
|
||||||
|
|
||||||
DefaultStorageClass = "STANDARD"
|
DefaultStorageClass = "STANDARD"
|
||||||
)
|
)
|
||||||
|
@ -125,7 +126,9 @@ var SystemMetadata = map[string]struct{}{
|
||||||
ContentLanguage: {},
|
ContentLanguage: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsSignedStreamingV4(r *http.Request) bool {
|
func IsSignedStreamingV4(r *http.Request) (string, bool) {
|
||||||
return r.Header.Get(AmzContentSha256) == StreamingContentSHA256 &&
|
shaHeader := r.Header.Get(AmzContentSha256)
|
||||||
r.Method == http.MethodPut
|
return shaHeader,
|
||||||
|
(shaHeader == StreamingContentSHA256 || shaHeader == StreamingContentV4aSHA256) &&
|
||||||
|
r.Method == http.MethodPut
|
||||||
}
|
}
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -12,6 +12,7 @@ require (
|
||||||
github.com/aws/aws-sdk-go v1.44.6
|
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 v1.30.3
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.27
|
||||||
|
github.com/aws/smithy-go v1.20.1
|
||||||
github.com/bluele/gcache v0.0.2
|
github.com/bluele/gcache v0.0.2
|
||||||
github.com/go-chi/chi/v5 v5.0.8
|
github.com/go-chi/chi/v5 v5.0.8
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
|
|
Loading…
Reference in a new issue