forked from TrueCloudLab/frostfs-s3-gw
2d58b25ea0
Signed-off-by: Angira Kekteeva <kira@nspcc.ru>
857 lines
28 KiB
Go
857 lines
28 KiB
Go
// Package v4 implements signing for AWS V4 signer
|
|
//
|
|
// Provides request signing for request that need to be signed with
|
|
// AWS V4 Signatures.
|
|
//
|
|
// # Standalone Signer
|
|
//
|
|
// Generally using the signer outside of the SDK should not require any additional
|
|
// logic when using Go v1.5 or higher. The signer does this by taking advantage
|
|
// of the URL.EscapedPath method. If your request URI requires additional escaping
|
|
// you many need to use the URL.Opaque to define what the raw URI should be sent
|
|
// to the service as.
|
|
//
|
|
// The signer will first check the URL.Opaque field, and use its value if set.
|
|
// The signer does require the URL.Opaque field to be set in the form of:
|
|
//
|
|
// "//<hostname>/<path>"
|
|
//
|
|
// // e.g.
|
|
// "//example.com/some/path"
|
|
//
|
|
// The leading "//" and hostname are required or the URL.Opaque escaping will
|
|
// not work correctly.
|
|
//
|
|
// If URL.Opaque is not set the signer will fallback to the URL.EscapedPath()
|
|
// method and using the returned value. If you're using Go v1.4 you must set
|
|
// URL.Opaque if the URI path needs escaping. If URL.Opaque is not set with
|
|
// Go v1.5 the signer will fallback to URL.Path.
|
|
//
|
|
// AWS v4 signature validation requires that the canonical string's URI path
|
|
// element must be the URI escaped form of the HTTP request's path.
|
|
// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
|
//
|
|
// The Go HTTP client will perform escaping automatically on the request. Some
|
|
// of these escaping may cause signature validation errors because the HTTP
|
|
// request differs from the URI path or query that the signature was generated.
|
|
// https://golang.org/pkg/net/url/#URL.EscapedPath
|
|
//
|
|
// Because of this, it is recommended that when using the signer outside of the
|
|
// SDK that explicitly escaping the request prior to being signed is preferable,
|
|
// and will help prevent signature validation errors. This can be done by setting
|
|
// the URL.Opaque or URL.RawPath. The SDK will use URL.Opaque first and then
|
|
// call URL.EscapedPath() if Opaque is not set.
|
|
//
|
|
// If signing a request intended for HTTP2 server, and you're using Go 1.6.2
|
|
// through 1.7.4 you should use the URL.RawPath as the pre-escaped form of the
|
|
// request URL. https://github.com/golang/go/issues/16847 points to a bug in
|
|
// Go pre 1.8 that fails to make HTTP2 requests using absolute URL in the HTTP
|
|
// message. URL.Opaque generally will force Go to make requests with absolute URL.
|
|
// URL.RawPath does not do this, but RawPath must be a valid escaping of Path
|
|
// or url.EscapedPath will ignore the RawPath escaping.
|
|
//
|
|
// Test `TestStandaloneSign` provides a complete example of using the signer
|
|
// outside of the SDK and pre-escaping the URI path.
|
|
package v4
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
|
"github.com/aws/aws-sdk-go/aws/request"
|
|
"github.com/aws/aws-sdk-go/private/protocol/rest"
|
|
)
|
|
|
|
const (
|
|
authorizationHeader = "Authorization"
|
|
authHeaderSignatureElem = "Signature="
|
|
signatureQueryKey = "X-Amz-Signature"
|
|
|
|
authHeaderPrefix = "AWS4-HMAC-SHA256"
|
|
timeFormat = "20060102T150405Z"
|
|
shortTimeFormat = "20060102"
|
|
awsV4Request = "aws4_request"
|
|
|
|
// emptyStringSHA256 is a SHA256 of an empty string.
|
|
emptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
|
|
)
|
|
|
|
var ignoredPresignHeaders = rules{
|
|
blacklist{
|
|
mapRule{
|
|
authorizationHeader: struct{}{},
|
|
"User-Agent": struct{}{},
|
|
"X-Amzn-Trace-Id": struct{}{},
|
|
},
|
|
},
|
|
}
|
|
|
|
// drop User-Agent header to be compatible with aws sdk java v1.
|
|
var ignoredHeaders = rules{
|
|
blacklist{
|
|
mapRule{
|
|
authorizationHeader: struct{}{},
|
|
"X-Amzn-Trace-Id": struct{}{},
|
|
},
|
|
},
|
|
}
|
|
|
|
// requiredSignedHeaders is a whitelist for build canonical headers.
|
|
var requiredSignedHeaders = rules{
|
|
whitelist{
|
|
mapRule{
|
|
"Cache-Control": struct{}{},
|
|
"Content-Disposition": struct{}{},
|
|
"Content-Encoding": struct{}{},
|
|
"Content-Language": struct{}{},
|
|
"Content-Md5": struct{}{},
|
|
"Content-Type": struct{}{},
|
|
"Expires": struct{}{},
|
|
"If-Match": struct{}{},
|
|
"If-Modified-Since": struct{}{},
|
|
"If-None-Match": struct{}{},
|
|
"If-Unmodified-Since": struct{}{},
|
|
"Range": struct{}{},
|
|
"X-Amz-Acl": struct{}{},
|
|
"X-Amz-Copy-Source": struct{}{},
|
|
"X-Amz-Copy-Source-If-Match": struct{}{},
|
|
"X-Amz-Copy-Source-If-Modified-Since": struct{}{},
|
|
"X-Amz-Copy-Source-If-None-Match": struct{}{},
|
|
"X-Amz-Copy-Source-If-Unmodified-Since": struct{}{},
|
|
"X-Amz-Copy-Source-Range": struct{}{},
|
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": struct{}{},
|
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": struct{}{},
|
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": struct{}{},
|
|
"X-Amz-Grant-Full-control": struct{}{},
|
|
"X-Amz-Grant-Read": struct{}{},
|
|
"X-Amz-Grant-Read-Acp": struct{}{},
|
|
"X-Amz-Grant-Write": struct{}{},
|
|
"X-Amz-Grant-Write-Acp": struct{}{},
|
|
"X-Amz-Metadata-Directive": struct{}{},
|
|
"X-Amz-Mfa": struct{}{},
|
|
"X-Amz-Request-Payer": struct{}{},
|
|
"X-Amz-Server-Side-Encryption": struct{}{},
|
|
"X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": struct{}{},
|
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": struct{}{},
|
|
"X-Amz-Server-Side-Encryption-Customer-Key": struct{}{},
|
|
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": struct{}{},
|
|
"X-Amz-Storage-Class": struct{}{},
|
|
"X-Amz-Tagging": struct{}{},
|
|
"X-Amz-Website-Redirect-Location": struct{}{},
|
|
"X-Amz-Content-Sha256": struct{}{},
|
|
},
|
|
},
|
|
patterns{"X-Amz-Meta-"},
|
|
}
|
|
|
|
// allowedHoisting is a whitelist for build query headers. The boolean value
|
|
// represents whether or not it is a pattern.
|
|
var allowedQueryHoisting = inclusiveRules{
|
|
blacklist{requiredSignedHeaders},
|
|
patterns{"X-Amz-"},
|
|
}
|
|
|
|
// Signer applies AWS v4 signing to given request. Use this to sign requests
|
|
// that need to be signed with AWS V4 Signatures.
|
|
type Signer struct {
|
|
// The authentication credentials the request will be signed against.
|
|
// This value must be set to sign requests.
|
|
Credentials *credentials.Credentials
|
|
|
|
// Sets the log level the signer should use when reporting information to
|
|
// the logger. If the logger is nil nothing will be logged. See
|
|
// aws.LogLevelType for more information on available logging levels
|
|
//
|
|
// By default nothing will be logged.
|
|
Debug aws.LogLevelType
|
|
|
|
// The logger loging information will be written to. If there the logger
|
|
// is nil, nothing will be logged.
|
|
Logger aws.Logger
|
|
|
|
// Disables the Signer's moving HTTP header key/value pairs from the HTTP
|
|
// request header to the request's query string. This is most commonly used
|
|
// with pre-signed requests preventing headers from being added to the
|
|
// request's query string.
|
|
DisableHeaderHoisting bool
|
|
|
|
// Disables the automatic escaping of the URI path of the request for the
|
|
// siganture's canonical string's path. For services that do not need additional
|
|
// escaping then use this to disable the signer escaping the path.
|
|
//
|
|
// S3 is an example of a service that does not need additional escaping.
|
|
//
|
|
// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
|
DisableURIPathEscaping bool
|
|
|
|
// Disables the automatical setting of the HTTP request's Body field with the
|
|
// io.ReadSeeker passed in to the signer. This is useful if you're using a
|
|
// custom wrapper around the body for the io.ReadSeeker and want to preserve
|
|
// the Body value on the Request.Body.
|
|
//
|
|
// This does run the risk of signing a request with a body that will not be
|
|
// sent in the request. Need to ensure that the underlying data of the Body
|
|
// values are the same.
|
|
DisableRequestBodyOverwrite bool
|
|
|
|
// currentTimeFn returns the time value which represents the current time.
|
|
// This value should only be used for testing. If it is nil the default
|
|
// time.Now will be used.
|
|
currentTimeFn func() time.Time
|
|
|
|
// UnsignedPayload will prevent signing of the payload. This will only
|
|
// work for services that have support for this.
|
|
UnsignedPayload bool
|
|
}
|
|
|
|
// NewSigner returns a Signer pointer configured with the credentials and optional
|
|
// option values provided. If not options are provided the Signer will use its
|
|
// default configuration.
|
|
func NewSigner(credentials *credentials.Credentials, options ...func(*Signer)) *Signer {
|
|
v4 := &Signer{
|
|
Credentials: credentials,
|
|
}
|
|
|
|
for _, option := range options {
|
|
option(v4)
|
|
}
|
|
|
|
return v4
|
|
}
|
|
|
|
type signingCtx struct {
|
|
ServiceName string
|
|
Region string
|
|
Request *http.Request
|
|
Body io.ReadSeeker
|
|
Query url.Values
|
|
Time time.Time
|
|
ExpireTime time.Duration
|
|
SignedHeaderVals http.Header
|
|
|
|
DisableURIPathEscaping bool
|
|
|
|
credValues credentials.Value
|
|
isPresign bool
|
|
unsignedPayload bool
|
|
|
|
bodyDigest string
|
|
signedHeaders string
|
|
canonicalHeaders string
|
|
canonicalString string
|
|
credentialString string
|
|
stringToSign string
|
|
signature string
|
|
}
|
|
|
|
// Sign signs AWS v4 requests with the provided body, service name, region the
|
|
// request is made to, and time the request is signed at. The signTime allows
|
|
// you to specify that a request is signed for the future, and cannot be
|
|
// used until then.
|
|
//
|
|
// Returns a list of HTTP headers that were included in the signature or an
|
|
// error if signing the request failed. Generally for signed requests this value
|
|
// is not needed as the full request context will be captured by the http.Request
|
|
// value. It is included for reference though.
|
|
//
|
|
// Sign will set the request's Body to be the `body` parameter passed in. If
|
|
// the body is not already an io.ReadCloser, it will be wrapped within one. If
|
|
// a `nil` body parameter passed to Sign, the request's Body field will be
|
|
// also set to nil. Its important to note that this functionality will not
|
|
// change the request's ContentLength of the request.
|
|
//
|
|
// Sign differs from Presign in that it will sign the request using HTTP
|
|
// header values. This type of signing is intended for http.Request values that
|
|
// will not be shared, or are shared in a way the header values on the request
|
|
// will not be lost.
|
|
//
|
|
// The requests body is an io.ReadSeeker so the SHA256 of the body can be
|
|
// generated. To bypass the signer computing the hash you can set the
|
|
// "X-Amz-Content-Sha256" header with a precomputed value. The signer will
|
|
// only compute the hash if the request header value is empty.
|
|
func (v4 Signer) Sign(r *http.Request, body io.ReadSeeker, service, region string, signTime time.Time) (http.Header, error) {
|
|
return v4.signWithBody(r, body, service, region, 0, false, signTime)
|
|
}
|
|
|
|
// Presign signs AWS v4 requests with the provided body, service name, region
|
|
// the request is made to, and time the request is signed at. The signTime
|
|
// allows you to specify that a request is signed for the future, and cannot
|
|
// be used until then.
|
|
//
|
|
// Returns a list of HTTP headers that were included in the signature or an
|
|
// error if signing the request failed. For presigned requests these headers
|
|
// and their values must be included on the HTTP request when it is made. This
|
|
// is helpful to know what header values need to be shared with the party the
|
|
// presigned request will be distributed to.
|
|
//
|
|
// Presign differs from Sign in that it will sign the request using query string
|
|
// instead of header values. This allows you to share the Presigned Request's
|
|
// URL with third parties, or distribute it throughout your system with minimal
|
|
// dependencies.
|
|
//
|
|
// Presign also takes an exp value which is the duration the
|
|
// signed request will be valid after the signing time. This is allows you to
|
|
// set when the request will expire.
|
|
//
|
|
// The requests body is an io.ReadSeeker so the SHA256 of the body can be
|
|
// generated. To bypass the signer computing the hash you can set the
|
|
// "X-Amz-Content-Sha256" header with a precomputed value. The signer will
|
|
// only compute the hash if the request header value is empty.
|
|
//
|
|
// Presigning a S3 request will not compute the body's SHA256 hash by default.
|
|
// This is done due to the general use case for S3 presigned URLs is to share
|
|
// PUT/GET capabilities. If you would like to include the body's SHA256 in the
|
|
// presigned request's signature you can set the "X-Amz-Content-Sha256"
|
|
// HTTP header and that will be included in the request's signature.
|
|
func (v4 Signer) Presign(r *http.Request, body io.ReadSeeker, service, region string, exp time.Duration, signTime time.Time) (http.Header, error) {
|
|
return v4.signWithBody(r, body, service, region, exp, true, signTime)
|
|
}
|
|
|
|
func (v4 Signer) signWithBody(r *http.Request, body io.ReadSeeker, service, region string, exp time.Duration, isPresign bool, signTime time.Time) (http.Header, error) {
|
|
currentTimeFn := v4.currentTimeFn
|
|
if currentTimeFn == nil {
|
|
currentTimeFn = time.Now
|
|
}
|
|
|
|
ctx := &signingCtx{
|
|
Request: r,
|
|
Body: body,
|
|
Query: r.URL.Query(),
|
|
Time: signTime,
|
|
ExpireTime: exp,
|
|
isPresign: isPresign,
|
|
ServiceName: service,
|
|
Region: region,
|
|
DisableURIPathEscaping: v4.DisableURIPathEscaping,
|
|
unsignedPayload: v4.UnsignedPayload,
|
|
}
|
|
|
|
for key := range ctx.Query {
|
|
sort.Strings(ctx.Query[key])
|
|
}
|
|
|
|
if ctx.isRequestSigned() {
|
|
ctx.Time = currentTimeFn()
|
|
ctx.handlePresignRemoval()
|
|
}
|
|
|
|
var err error
|
|
ctx.credValues, err = v4.Credentials.GetWithContext(requestContext(r))
|
|
if err != nil {
|
|
return http.Header{}, err
|
|
}
|
|
|
|
ctx.sanitizeHostForHeader()
|
|
ctx.assignAmzQueryValues()
|
|
if err := ctx.build(v4.DisableHeaderHoisting); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If the request is not presigned the body should be attached to it. This
|
|
// prevents the confusion of wanting to send a signed request without
|
|
// the body the request was signed for attached.
|
|
if !(v4.DisableRequestBodyOverwrite || ctx.isPresign) {
|
|
var reader io.ReadCloser
|
|
if body != nil {
|
|
var ok bool
|
|
if reader, ok = body.(io.ReadCloser); !ok {
|
|
reader = ioutil.NopCloser(body)
|
|
}
|
|
}
|
|
r.Body = reader
|
|
}
|
|
|
|
if v4.Debug.Matches(aws.LogDebugWithSigning) {
|
|
v4.logSigningInfo(ctx)
|
|
}
|
|
|
|
return ctx.SignedHeaderVals, nil
|
|
}
|
|
|
|
func (ctx *signingCtx) sanitizeHostForHeader() {
|
|
request.SanitizeHostForHeader(ctx.Request)
|
|
}
|
|
|
|
func (ctx *signingCtx) handlePresignRemoval() {
|
|
if !ctx.isPresign {
|
|
return
|
|
}
|
|
|
|
// The credentials have expired for this request. The current signing
|
|
// is invalid, and needs to be request because the request will fail.
|
|
ctx.removePresign()
|
|
|
|
// Update the request's query string to ensure the values stays in
|
|
// sync in the case retrieving the new credentials fails.
|
|
ctx.Request.URL.RawQuery = ctx.Query.Encode()
|
|
}
|
|
|
|
func (ctx *signingCtx) assignAmzQueryValues() {
|
|
if ctx.isPresign {
|
|
ctx.Query.Set("X-Amz-Algorithm", authHeaderPrefix)
|
|
if ctx.credValues.SessionToken != "" {
|
|
ctx.Query.Set("X-Amz-Security-Token", ctx.credValues.SessionToken)
|
|
} else {
|
|
ctx.Query.Del("X-Amz-Security-Token")
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
if ctx.credValues.SessionToken != "" {
|
|
ctx.Request.Header.Set("X-Amz-Security-Token", ctx.credValues.SessionToken)
|
|
}
|
|
}
|
|
|
|
// SignRequestHandler is a named request handler the SDK will use to sign
|
|
// service client request with using the V4 signature.
|
|
var SignRequestHandler = request.NamedHandler{
|
|
Name: "v4.SignRequestHandler", Fn: SignSDKRequest,
|
|
}
|
|
|
|
// SignSDKRequest signs an AWS request with the V4 signature. This
|
|
// request handler should only be used with the SDK's built in service client's
|
|
// API operation requests.
|
|
//
|
|
// This function should not be used on its on its own, but in conjunction with
|
|
// an AWS service client's API operation call. To sign a standalone request
|
|
// not created by a service client's API operation method use the "Sign" or
|
|
// "Presign" functions of the "Signer" type.
|
|
//
|
|
// If the credentials of the request's config are set to
|
|
// credentials.AnonymousCredentials the request will not be signed.
|
|
func SignSDKRequest(req *request.Request) {
|
|
SignSDKRequestWithCurrentTime(req, time.Now)
|
|
}
|
|
|
|
// BuildNamedHandler will build a generic handler for signing.
|
|
func BuildNamedHandler(name string, opts ...func(*Signer)) request.NamedHandler {
|
|
return request.NamedHandler{
|
|
Name: name,
|
|
Fn: func(req *request.Request) {
|
|
SignSDKRequestWithCurrentTime(req, time.Now, opts...)
|
|
},
|
|
}
|
|
}
|
|
|
|
// SignSDKRequestWithCurrentTime will sign the SDK's request using the time
|
|
// function passed in. Behaves the same as SignSDKRequest with the exception
|
|
// the request is signed with the value returned by the current time function.
|
|
func SignSDKRequestWithCurrentTime(req *request.Request, curTimeFn func() time.Time, opts ...func(*Signer)) {
|
|
// If the request does not need to be signed ignore the signing of the
|
|
// request if the AnonymousCredentials object is used.
|
|
if req.Config.Credentials == credentials.AnonymousCredentials {
|
|
return
|
|
}
|
|
|
|
region := req.ClientInfo.SigningRegion
|
|
if region == "" {
|
|
region = aws.StringValue(req.Config.Region)
|
|
}
|
|
|
|
name := req.ClientInfo.SigningName
|
|
if name == "" {
|
|
name = req.ClientInfo.ServiceName
|
|
}
|
|
|
|
v4 := NewSigner(req.Config.Credentials, func(v4 *Signer) {
|
|
v4.Debug = req.Config.LogLevel.Value()
|
|
v4.Logger = req.Config.Logger
|
|
v4.DisableHeaderHoisting = req.NotHoist
|
|
v4.currentTimeFn = curTimeFn
|
|
if name == "s3" {
|
|
// S3 service should not have any escaping applied
|
|
v4.DisableURIPathEscaping = true
|
|
}
|
|
// Prevents setting the HTTPRequest's Body. Since the Body could be
|
|
// wrapped in a custom io.Closer that we do not want to be stompped
|
|
// on top of by the signer.
|
|
v4.DisableRequestBodyOverwrite = true
|
|
})
|
|
|
|
for _, opt := range opts {
|
|
opt(v4)
|
|
}
|
|
|
|
curTime := curTimeFn()
|
|
signedHeaders, err := v4.signWithBody(req.HTTPRequest, req.GetBody(),
|
|
name, region, req.ExpireTime, req.ExpireTime > 0, curTime,
|
|
)
|
|
if err != nil {
|
|
req.Error = err
|
|
req.SignedHeaderVals = nil
|
|
return
|
|
}
|
|
|
|
req.SignedHeaderVals = signedHeaders
|
|
req.LastSignedAt = curTime
|
|
}
|
|
|
|
const logSignInfoMsg = `DEBUG: Request Signature:
|
|
---[ CANONICAL STRING ]-----------------------------
|
|
%s
|
|
---[ STRING TO SIGN ]--------------------------------
|
|
%s%s
|
|
-----------------------------------------------------`
|
|
const logSignedURLMsg = `
|
|
---[ SIGNED URL ]------------------------------------
|
|
%s`
|
|
|
|
func (v4 *Signer) logSigningInfo(ctx *signingCtx) {
|
|
signedURLMsg := ""
|
|
if ctx.isPresign {
|
|
signedURLMsg = fmt.Sprintf(logSignedURLMsg, ctx.Request.URL.String())
|
|
}
|
|
msg := fmt.Sprintf(logSignInfoMsg, ctx.canonicalString, ctx.stringToSign, signedURLMsg)
|
|
v4.Logger.Log(msg)
|
|
}
|
|
|
|
func (ctx *signingCtx) build(disableHeaderHoisting bool) error {
|
|
ctx.buildTime() // no depends
|
|
ctx.buildCredentialString() // no depends
|
|
|
|
if err := ctx.buildBodyDigest(); err != nil {
|
|
return err
|
|
}
|
|
|
|
unsignedHeaders := ctx.Request.Header
|
|
if ctx.isPresign {
|
|
if !disableHeaderHoisting {
|
|
var urlValues url.Values
|
|
urlValues, unsignedHeaders = buildQuery(allowedQueryHoisting, unsignedHeaders) // no depends
|
|
for k := range urlValues {
|
|
ctx.Query[k] = urlValues[k]
|
|
}
|
|
}
|
|
}
|
|
|
|
if ctx.isPresign {
|
|
ctx.buildCanonicalHeaders(ignoredPresignHeaders, unsignedHeaders)
|
|
} else {
|
|
ctx.buildCanonicalHeaders(ignoredHeaders, unsignedHeaders)
|
|
}
|
|
ctx.buildCanonicalString() // depends on canon headers / signed headers
|
|
ctx.buildStringToSign() // depends on canon string
|
|
ctx.buildSignature() // depends on string to sign
|
|
|
|
if ctx.isPresign {
|
|
ctx.Request.URL.RawQuery += "&" + signatureQueryKey + "=" + ctx.signature
|
|
} else {
|
|
parts := []string{
|
|
authHeaderPrefix + " Credential=" + ctx.credValues.AccessKeyID + "/" + ctx.credentialString,
|
|
"SignedHeaders=" + ctx.signedHeaders,
|
|
authHeaderSignatureElem + ctx.signature,
|
|
}
|
|
ctx.Request.Header.Set(authorizationHeader, strings.Join(parts, ", "))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetSignedRequestSignature attempts to extract the signature of the request.
|
|
// Returning an error if the request is unsigned, or unable to extract the
|
|
// signature.
|
|
func GetSignedRequestSignature(r *http.Request) ([]byte, error) {
|
|
if auth := r.Header.Get(authorizationHeader); len(auth) != 0 {
|
|
ps := strings.Split(auth, ", ")
|
|
for _, p := range ps {
|
|
if idx := strings.Index(p, authHeaderSignatureElem); idx >= 0 {
|
|
sig := p[len(authHeaderSignatureElem):]
|
|
if len(sig) == 0 {
|
|
return nil, fmt.Errorf("invalid request signature authorization header")
|
|
}
|
|
return hex.DecodeString(sig)
|
|
}
|
|
}
|
|
}
|
|
|
|
if sig := r.URL.Query().Get("X-Amz-Signature"); len(sig) != 0 {
|
|
return hex.DecodeString(sig)
|
|
}
|
|
|
|
return nil, fmt.Errorf("request not signed")
|
|
}
|
|
|
|
func (ctx *signingCtx) buildTime() {
|
|
if ctx.isPresign {
|
|
duration := int64(ctx.ExpireTime / time.Second)
|
|
ctx.Query.Set("X-Amz-Date", formatTime(ctx.Time))
|
|
ctx.Query.Set("X-Amz-Expires", strconv.FormatInt(duration, 10))
|
|
} else {
|
|
ctx.Request.Header.Set("X-Amz-Date", formatTime(ctx.Time))
|
|
}
|
|
}
|
|
|
|
func (ctx *signingCtx) buildCredentialString() {
|
|
ctx.credentialString = buildSigningScope(ctx.Region, ctx.ServiceName, ctx.Time)
|
|
|
|
if ctx.isPresign {
|
|
ctx.Query.Set("X-Amz-Credential", ctx.credValues.AccessKeyID+"/"+ctx.credentialString)
|
|
}
|
|
}
|
|
|
|
func buildQuery(r rule, header http.Header) (url.Values, http.Header) {
|
|
query := url.Values{}
|
|
unsignedHeaders := http.Header{}
|
|
for k, h := range header {
|
|
if r.IsValid(k) {
|
|
query[k] = h
|
|
} else {
|
|
unsignedHeaders[k] = h
|
|
}
|
|
}
|
|
|
|
return query, unsignedHeaders
|
|
}
|
|
func (ctx *signingCtx) buildCanonicalHeaders(r rule, header http.Header) {
|
|
var headers []string
|
|
headers = append(headers, "host")
|
|
for k, v := range header {
|
|
if !r.IsValid(k) {
|
|
continue // ignored header
|
|
}
|
|
if ctx.SignedHeaderVals == nil {
|
|
ctx.SignedHeaderVals = make(http.Header)
|
|
}
|
|
|
|
lowerCaseKey := strings.ToLower(k)
|
|
if _, ok := ctx.SignedHeaderVals[lowerCaseKey]; ok {
|
|
// include additional values
|
|
ctx.SignedHeaderVals[lowerCaseKey] = append(ctx.SignedHeaderVals[lowerCaseKey], v...)
|
|
continue
|
|
}
|
|
|
|
headers = append(headers, lowerCaseKey)
|
|
ctx.SignedHeaderVals[lowerCaseKey] = v
|
|
}
|
|
sort.Strings(headers)
|
|
|
|
ctx.signedHeaders = strings.Join(headers, ";")
|
|
|
|
if ctx.isPresign {
|
|
ctx.Query.Set("X-Amz-SignedHeaders", ctx.signedHeaders)
|
|
}
|
|
|
|
headerValues := make([]string, len(headers))
|
|
for i, k := range headers {
|
|
if k == "host" {
|
|
if ctx.Request.Host != "" {
|
|
headerValues[i] = "host:" + ctx.Request.Host
|
|
} else {
|
|
headerValues[i] = "host:" + ctx.Request.URL.Host
|
|
}
|
|
} else {
|
|
headerValues[i] = k + ":" +
|
|
strings.Join(ctx.SignedHeaderVals[k], ",")
|
|
}
|
|
}
|
|
stripExcessSpaces(headerValues)
|
|
ctx.canonicalHeaders = strings.Join(headerValues, "\n")
|
|
}
|
|
|
|
func (ctx *signingCtx) buildCanonicalString() {
|
|
ctx.Request.URL.RawQuery = strings.Replace(ctx.Query.Encode(), "+", "%20", -1)
|
|
|
|
uri := getURIPath(ctx.Request.URL)
|
|
|
|
if !ctx.DisableURIPathEscaping {
|
|
uri = rest.EscapePath(uri, false)
|
|
}
|
|
|
|
ctx.canonicalString = strings.Join([]string{
|
|
ctx.Request.Method,
|
|
uri,
|
|
ctx.Request.URL.RawQuery,
|
|
ctx.canonicalHeaders + "\n",
|
|
ctx.signedHeaders,
|
|
ctx.bodyDigest,
|
|
}, "\n")
|
|
}
|
|
|
|
func (ctx *signingCtx) buildStringToSign() {
|
|
ctx.stringToSign = strings.Join([]string{
|
|
authHeaderPrefix,
|
|
formatTime(ctx.Time),
|
|
ctx.credentialString,
|
|
hex.EncodeToString(hashSHA256([]byte(ctx.canonicalString))),
|
|
}, "\n")
|
|
}
|
|
|
|
func (ctx *signingCtx) buildSignature() {
|
|
creds := deriveSigningKey(ctx.Region, ctx.ServiceName, ctx.credValues.SecretAccessKey, ctx.Time)
|
|
signature := hmacSHA256(creds, []byte(ctx.stringToSign))
|
|
ctx.signature = hex.EncodeToString(signature)
|
|
}
|
|
|
|
func (ctx *signingCtx) buildBodyDigest() error {
|
|
hash := ctx.Request.Header.Get("X-Amz-Content-Sha256")
|
|
if hash == "" {
|
|
includeSHA256Header := ctx.unsignedPayload ||
|
|
ctx.ServiceName == "s3" ||
|
|
ctx.ServiceName == "glacier"
|
|
|
|
s3Presign := ctx.isPresign && ctx.ServiceName == "s3"
|
|
|
|
if ctx.unsignedPayload || s3Presign {
|
|
hash = "UNSIGNED-PAYLOAD"
|
|
includeSHA256Header = !s3Presign
|
|
} else if ctx.Body == nil {
|
|
hash = emptyStringSHA256
|
|
} else {
|
|
if !aws.IsReaderSeekable(ctx.Body) {
|
|
return fmt.Errorf("cannot use unseekable request body %T, for signed request with body", ctx.Body)
|
|
}
|
|
hashBytes, err := makeSha256Reader(ctx.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hash = hex.EncodeToString(hashBytes)
|
|
}
|
|
|
|
if includeSHA256Header {
|
|
ctx.Request.Header.Set("X-Amz-Content-Sha256", hash)
|
|
}
|
|
}
|
|
ctx.bodyDigest = hash
|
|
|
|
return nil
|
|
}
|
|
|
|
// isRequestSigned returns if the request is currently signed or presigned.
|
|
func (ctx *signingCtx) isRequestSigned() bool {
|
|
if ctx.isPresign && ctx.Query.Get("X-Amz-Signature") != "" {
|
|
return true
|
|
}
|
|
if ctx.Request.Header.Get("Authorization") != "" {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// unsign removes signing flags for both signed and presigned requests.
|
|
func (ctx *signingCtx) removePresign() {
|
|
ctx.Query.Del("X-Amz-Algorithm")
|
|
ctx.Query.Del("X-Amz-Signature")
|
|
ctx.Query.Del("X-Amz-Security-Token")
|
|
ctx.Query.Del("X-Amz-Date")
|
|
ctx.Query.Del("X-Amz-Expires")
|
|
ctx.Query.Del("X-Amz-Credential")
|
|
ctx.Query.Del("X-Amz-SignedHeaders")
|
|
}
|
|
|
|
func hmacSHA256(key []byte, data []byte) []byte {
|
|
hash := hmac.New(sha256.New, key)
|
|
hash.Write(data)
|
|
return hash.Sum(nil)
|
|
}
|
|
|
|
func hashSHA256(data []byte) []byte {
|
|
hash := sha256.New()
|
|
hash.Write(data)
|
|
return hash.Sum(nil)
|
|
}
|
|
|
|
func makeSha256Reader(reader io.ReadSeeker) (hashBytes []byte, err error) {
|
|
hash := sha256.New()
|
|
start, err := reader.Seek(0, io.SeekCurrent)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
// ensure error is return if unable to seek back to start of payload.
|
|
_, err = reader.Seek(start, io.SeekStart)
|
|
}()
|
|
|
|
// Use CopyN to avoid allocating the 32KB buffer in io.Copy for bodies
|
|
// smaller than 32KB. Fall back to io.Copy if we fail to determine the size.
|
|
size, err := aws.SeekerLen(reader)
|
|
if err != nil {
|
|
_, _ = io.Copy(hash, reader)
|
|
} else {
|
|
_, _ = io.CopyN(hash, reader, size)
|
|
}
|
|
|
|
return hash.Sum(nil), nil
|
|
}
|
|
|
|
const doubleSpace = " "
|
|
|
|
// stripExcessSpaces will rewrite the passed in slice's string values to not
|
|
// contain multiple side-by-side spaces.
|
|
func stripExcessSpaces(vals []string) {
|
|
var j, k, l, m, spaces int
|
|
for i, str := range vals {
|
|
// Trim trailing spaces
|
|
for j = len(str) - 1; j >= 0 && str[j] == ' '; j-- {
|
|
}
|
|
|
|
// Trim leading spaces
|
|
for k = 0; k < j && str[k] == ' '; k++ {
|
|
}
|
|
str = str[k : j+1]
|
|
|
|
// Strip multiple spaces.
|
|
j = strings.Index(str, doubleSpace)
|
|
if j < 0 {
|
|
vals[i] = str
|
|
continue
|
|
}
|
|
|
|
buf := []byte(str)
|
|
for k, m, l = j, j, len(buf); k < l; k++ {
|
|
if buf[k] == ' ' {
|
|
if spaces == 0 {
|
|
// First space.
|
|
buf[m] = buf[k]
|
|
m++
|
|
}
|
|
spaces++
|
|
} else {
|
|
// End of multiple spaces.
|
|
spaces = 0
|
|
buf[m] = buf[k]
|
|
m++
|
|
}
|
|
}
|
|
|
|
vals[i] = string(buf[:m])
|
|
}
|
|
}
|
|
|
|
func buildSigningScope(region, service string, dt time.Time) string {
|
|
return strings.Join([]string{
|
|
formatShortTime(dt),
|
|
region,
|
|
service,
|
|
awsV4Request,
|
|
}, "/")
|
|
}
|
|
|
|
func deriveSigningKey(region, service, secretKey string, dt time.Time) []byte {
|
|
hmacDate := hmacSHA256([]byte("AWS4"+secretKey), []byte(formatShortTime(dt)))
|
|
hmacRegion := hmacSHA256(hmacDate, []byte(region))
|
|
hmacService := hmacSHA256(hmacRegion, []byte(service))
|
|
signingKey := hmacSHA256(hmacService, []byte(awsV4Request))
|
|
return signingKey
|
|
}
|
|
|
|
func formatShortTime(dt time.Time) string {
|
|
return dt.UTC().Format(shortTimeFormat)
|
|
}
|
|
|
|
func formatTime(dt time.Time) string {
|
|
return dt.UTC().Format(timeFormat)
|
|
}
|