src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

v4.go (18405B)


      1 // Package v4 implements signing for AWS V4 signer
      2 //
      3 // Provides request signing for request that need to be signed with
      4 // AWS V4 Signatures.
      5 //
      6 // # Standalone Signer
      7 //
      8 // Generally using the signer outside of the SDK should not require any additional
      9 //
     10 //	The signer does this by taking advantage of the URL.EscapedPath method. If your request URI requires
     11 //
     12 // additional escaping you many need to use the URL.Opaque to define what the raw URI should be sent
     13 // to the service as.
     14 //
     15 // The signer will first check the URL.Opaque field, and use its value if set.
     16 // The signer does require the URL.Opaque field to be set in the form of:
     17 //
     18 //	"//<hostname>/<path>"
     19 //
     20 //	// e.g.
     21 //	"//example.com/some/path"
     22 //
     23 // The leading "//" and hostname are required or the URL.Opaque escaping will
     24 // not work correctly.
     25 //
     26 // If URL.Opaque is not set the signer will fallback to the URL.EscapedPath()
     27 // method and using the returned value.
     28 //
     29 // AWS v4 signature validation requires that the canonical string's URI path
     30 // element must be the URI escaped form of the HTTP request's path.
     31 // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
     32 //
     33 // The Go HTTP client will perform escaping automatically on the request. Some
     34 // of these escaping may cause signature validation errors because the HTTP
     35 // request differs from the URI path or query that the signature was generated.
     36 // https://golang.org/pkg/net/url/#URL.EscapedPath
     37 //
     38 // Because of this, it is recommended that when using the signer outside of the
     39 // SDK that explicitly escaping the request prior to being signed is preferable,
     40 // and will help prevent signature validation errors. This can be done by setting
     41 // the URL.Opaque or URL.RawPath. The SDK will use URL.Opaque first and then
     42 // call URL.EscapedPath() if Opaque is not set.
     43 //
     44 // Test `TestStandaloneSign` provides a complete example of using the signer
     45 // outside of the SDK and pre-escaping the URI path.
     46 package v4
     47 
     48 import (
     49 	"context"
     50 	"crypto/sha256"
     51 	"encoding/hex"
     52 	"fmt"
     53 	"hash"
     54 	"net/http"
     55 	"net/textproto"
     56 	"net/url"
     57 	"sort"
     58 	"strconv"
     59 	"strings"
     60 	"time"
     61 
     62 	"github.com/aws/aws-sdk-go-v2/aws"
     63 	v4Internal "github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4"
     64 	"github.com/aws/smithy-go/encoding/httpbinding"
     65 	"github.com/aws/smithy-go/logging"
     66 )
     67 
     68 const (
     69 	signingAlgorithm    = "AWS4-HMAC-SHA256"
     70 	authorizationHeader = "Authorization"
     71 
     72 	// Version of signing v4
     73 	Version = "SigV4"
     74 )
     75 
     76 // HTTPSigner is an interface to a SigV4 signer that can sign HTTP requests
     77 type HTTPSigner interface {
     78 	SignHTTP(ctx context.Context, credentials aws.Credentials, r *http.Request, payloadHash string, service string, region string, signingTime time.Time, optFns ...func(*SignerOptions)) error
     79 }
     80 
     81 type keyDerivator interface {
     82 	DeriveKey(credential aws.Credentials, service, region string, signingTime v4Internal.SigningTime) []byte
     83 }
     84 
     85 // SignerOptions is the SigV4 Signer options.
     86 type SignerOptions struct {
     87 	// Disables the Signer's moving HTTP header key/value pairs from the HTTP
     88 	// request header to the request's query string. This is most commonly used
     89 	// with pre-signed requests preventing headers from being added to the
     90 	// request's query string.
     91 	DisableHeaderHoisting bool
     92 
     93 	// Disables the automatic escaping of the URI path of the request for the
     94 	// siganture's canonical string's path. For services that do not need additional
     95 	// escaping then use this to disable the signer escaping the path.
     96 	//
     97 	// S3 is an example of a service that does not need additional escaping.
     98 	//
     99 	// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
    100 	DisableURIPathEscaping bool
    101 
    102 	// The logger to send log messages to.
    103 	Logger logging.Logger
    104 
    105 	// Enable logging of signed requests.
    106 	// This will enable logging of the canonical request, the string to sign, and for presigning the subsequent
    107 	// presigned URL.
    108 	LogSigning bool
    109 
    110 	// Disables setting the session token on the request as part of signing
    111 	// through X-Amz-Security-Token. This is needed for variations of v4 that
    112 	// present the token elsewhere.
    113 	DisableSessionToken bool
    114 }
    115 
    116 // Signer applies AWS v4 signing to given request. Use this to sign requests
    117 // that need to be signed with AWS V4 Signatures.
    118 type Signer struct {
    119 	options      SignerOptions
    120 	keyDerivator keyDerivator
    121 }
    122 
    123 // NewSigner returns a new SigV4 Signer
    124 func NewSigner(optFns ...func(signer *SignerOptions)) *Signer {
    125 	options := SignerOptions{}
    126 
    127 	for _, fn := range optFns {
    128 		fn(&options)
    129 	}
    130 
    131 	return &Signer{options: options, keyDerivator: v4Internal.NewSigningKeyDeriver()}
    132 }
    133 
    134 type httpSigner struct {
    135 	Request      *http.Request
    136 	ServiceName  string
    137 	Region       string
    138 	Time         v4Internal.SigningTime
    139 	Credentials  aws.Credentials
    140 	KeyDerivator keyDerivator
    141 	IsPreSign    bool
    142 
    143 	PayloadHash string
    144 
    145 	DisableHeaderHoisting  bool
    146 	DisableURIPathEscaping bool
    147 	DisableSessionToken    bool
    148 }
    149 
    150 func (s *httpSigner) Build() (signedRequest, error) {
    151 	req := s.Request
    152 
    153 	query := req.URL.Query()
    154 	headers := req.Header
    155 
    156 	s.setRequiredSigningFields(headers, query)
    157 
    158 	// Sort Each Query Key's Values
    159 	for key := range query {
    160 		sort.Strings(query[key])
    161 	}
    162 
    163 	v4Internal.SanitizeHostForHeader(req)
    164 
    165 	credentialScope := s.buildCredentialScope()
    166 	credentialStr := s.Credentials.AccessKeyID + "/" + credentialScope
    167 	if s.IsPreSign {
    168 		query.Set(v4Internal.AmzCredentialKey, credentialStr)
    169 	}
    170 
    171 	unsignedHeaders := headers
    172 	if s.IsPreSign && !s.DisableHeaderHoisting {
    173 		var urlValues url.Values
    174 		urlValues, unsignedHeaders = buildQuery(v4Internal.AllowedQueryHoisting, headers)
    175 		for k := range urlValues {
    176 			query[k] = urlValues[k]
    177 		}
    178 	}
    179 
    180 	host := req.URL.Host
    181 	if len(req.Host) > 0 {
    182 		host = req.Host
    183 	}
    184 
    185 	signedHeaders, signedHeadersStr, canonicalHeaderStr := s.buildCanonicalHeaders(host, v4Internal.IgnoredHeaders, unsignedHeaders, s.Request.ContentLength)
    186 
    187 	if s.IsPreSign {
    188 		query.Set(v4Internal.AmzSignedHeadersKey, signedHeadersStr)
    189 	}
    190 
    191 	var rawQuery strings.Builder
    192 	rawQuery.WriteString(strings.Replace(query.Encode(), "+", "%20", -1))
    193 
    194 	canonicalURI := v4Internal.GetURIPath(req.URL)
    195 	if !s.DisableURIPathEscaping {
    196 		canonicalURI = httpbinding.EscapePath(canonicalURI, false)
    197 	}
    198 
    199 	canonicalString := s.buildCanonicalString(
    200 		req.Method,
    201 		canonicalURI,
    202 		rawQuery.String(),
    203 		signedHeadersStr,
    204 		canonicalHeaderStr,
    205 	)
    206 
    207 	strToSign := s.buildStringToSign(credentialScope, canonicalString)
    208 	signingSignature, err := s.buildSignature(strToSign)
    209 	if err != nil {
    210 		return signedRequest{}, err
    211 	}
    212 
    213 	if s.IsPreSign {
    214 		rawQuery.WriteString("&X-Amz-Signature=")
    215 		rawQuery.WriteString(signingSignature)
    216 	} else {
    217 		headers[authorizationHeader] = append(headers[authorizationHeader][:0], buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature))
    218 	}
    219 
    220 	req.URL.RawQuery = rawQuery.String()
    221 
    222 	return signedRequest{
    223 		Request:         req,
    224 		SignedHeaders:   signedHeaders,
    225 		CanonicalString: canonicalString,
    226 		StringToSign:    strToSign,
    227 		PreSigned:       s.IsPreSign,
    228 	}, nil
    229 }
    230 
    231 func buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature string) string {
    232 	const credential = "Credential="
    233 	const signedHeaders = "SignedHeaders="
    234 	const signature = "Signature="
    235 	const commaSpace = ", "
    236 
    237 	var parts strings.Builder
    238 	parts.Grow(len(signingAlgorithm) + 1 +
    239 		len(credential) + len(credentialStr) + 2 +
    240 		len(signedHeaders) + len(signedHeadersStr) + 2 +
    241 		len(signature) + len(signingSignature),
    242 	)
    243 	parts.WriteString(signingAlgorithm)
    244 	parts.WriteRune(' ')
    245 	parts.WriteString(credential)
    246 	parts.WriteString(credentialStr)
    247 	parts.WriteString(commaSpace)
    248 	parts.WriteString(signedHeaders)
    249 	parts.WriteString(signedHeadersStr)
    250 	parts.WriteString(commaSpace)
    251 	parts.WriteString(signature)
    252 	parts.WriteString(signingSignature)
    253 	return parts.String()
    254 }
    255 
    256 // SignHTTP signs AWS v4 requests with the provided payload hash, service name, region the
    257 // request is made to, and time the request is signed at. The signTime allows
    258 // you to specify that a request is signed for the future, and cannot be
    259 // used until then.
    260 //
    261 // The payloadHash is the hex encoded SHA-256 hash of the request payload, and
    262 // must be provided. Even if the request has no payload (aka body). If the
    263 // request has no payload you should use the hex encoded SHA-256 of an empty
    264 // string as the payloadHash value.
    265 //
    266 //	"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
    267 //
    268 // Some services such as Amazon S3 accept alternative values for the payload
    269 // hash, such as "UNSIGNED-PAYLOAD" for requests where the body will not be
    270 // included in the request signature.
    271 //
    272 // https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
    273 //
    274 // Sign differs from Presign in that it will sign the request using HTTP
    275 // header values. This type of signing is intended for http.Request values that
    276 // will not be shared, or are shared in a way the header values on the request
    277 // will not be lost.
    278 //
    279 // The passed in request will be modified in place.
    280 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 {
    281 	options := s.options
    282 
    283 	for _, fn := range optFns {
    284 		fn(&options)
    285 	}
    286 
    287 	signer := &httpSigner{
    288 		Request:                r,
    289 		PayloadHash:            payloadHash,
    290 		ServiceName:            service,
    291 		Region:                 region,
    292 		Credentials:            credentials,
    293 		Time:                   v4Internal.NewSigningTime(signingTime.UTC()),
    294 		DisableHeaderHoisting:  options.DisableHeaderHoisting,
    295 		DisableURIPathEscaping: options.DisableURIPathEscaping,
    296 		DisableSessionToken:    options.DisableSessionToken,
    297 		KeyDerivator:           s.keyDerivator,
    298 	}
    299 
    300 	signedRequest, err := signer.Build()
    301 	if err != nil {
    302 		return err
    303 	}
    304 
    305 	logSigningInfo(ctx, options, &signedRequest, false)
    306 
    307 	return nil
    308 }
    309 
    310 // PresignHTTP signs AWS v4 requests with the payload hash, service name, region
    311 // the request is made to, and time the request is signed at. The signTime
    312 // allows you to specify that a request is signed for the future, and cannot
    313 // be used until then.
    314 //
    315 // Returns the signed URL and the map of HTTP headers that were included in the
    316 // signature or an error if signing the request failed. For presigned requests
    317 // these headers and their values must be included on the HTTP request when it
    318 // is made. This is helpful to know what header values need to be shared with
    319 // the party the presigned request will be distributed to.
    320 //
    321 // The payloadHash is the hex encoded SHA-256 hash of the request payload, and
    322 // must be provided. Even if the request has no payload (aka body). If the
    323 // request has no payload you should use the hex encoded SHA-256 of an empty
    324 // string as the payloadHash value.
    325 //
    326 //	"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
    327 //
    328 // Some services such as Amazon S3 accept alternative values for the payload
    329 // hash, such as "UNSIGNED-PAYLOAD" for requests where the body will not be
    330 // included in the request signature.
    331 //
    332 // https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
    333 //
    334 // PresignHTTP differs from SignHTTP in that it will sign the request using
    335 // query string instead of header values. This allows you to share the
    336 // Presigned Request's URL with third parties, or distribute it throughout your
    337 // system with minimal dependencies.
    338 //
    339 // PresignHTTP will not set the expires time of the presigned request
    340 // automatically. To specify the expire duration for a request add the
    341 // "X-Amz-Expires" query parameter on the request with the value as the
    342 // duration in seconds the presigned URL should be considered valid for. This
    343 // parameter is not used by all AWS services, and is most notable used by
    344 // Amazon S3 APIs.
    345 //
    346 //	expires := 20 * time.Minute
    347 //	query := req.URL.Query()
    348 //	query.Set("X-Amz-Expires", strconv.FormatInt(int64(expires/time.Second), 10))
    349 //	req.URL.RawQuery = query.Encode()
    350 //
    351 // This method does not modify the provided request.
    352 func (s *Signer) PresignHTTP(
    353 	ctx context.Context, credentials aws.Credentials, r *http.Request,
    354 	payloadHash string, service string, region string, signingTime time.Time,
    355 	optFns ...func(*SignerOptions),
    356 ) (signedURI string, signedHeaders http.Header, err error) {
    357 	options := s.options
    358 
    359 	for _, fn := range optFns {
    360 		fn(&options)
    361 	}
    362 
    363 	signer := &httpSigner{
    364 		Request:                r.Clone(r.Context()),
    365 		PayloadHash:            payloadHash,
    366 		ServiceName:            service,
    367 		Region:                 region,
    368 		Credentials:            credentials,
    369 		Time:                   v4Internal.NewSigningTime(signingTime.UTC()),
    370 		IsPreSign:              true,
    371 		DisableHeaderHoisting:  options.DisableHeaderHoisting,
    372 		DisableURIPathEscaping: options.DisableURIPathEscaping,
    373 		DisableSessionToken:    options.DisableSessionToken,
    374 		KeyDerivator:           s.keyDerivator,
    375 	}
    376 
    377 	signedRequest, err := signer.Build()
    378 	if err != nil {
    379 		return "", nil, err
    380 	}
    381 
    382 	logSigningInfo(ctx, options, &signedRequest, true)
    383 
    384 	signedHeaders = make(http.Header)
    385 
    386 	// For the signed headers we canonicalize the header keys in the returned map.
    387 	// This avoids situations where can standard library double headers like host header. For example the standard
    388 	// library will set the Host header, even if it is present in lower-case form.
    389 	for k, v := range signedRequest.SignedHeaders {
    390 		key := textproto.CanonicalMIMEHeaderKey(k)
    391 		signedHeaders[key] = append(signedHeaders[key], v...)
    392 	}
    393 
    394 	return signedRequest.Request.URL.String(), signedHeaders, nil
    395 }
    396 
    397 func (s *httpSigner) buildCredentialScope() string {
    398 	return v4Internal.BuildCredentialScope(s.Time, s.Region, s.ServiceName)
    399 }
    400 
    401 func buildQuery(r v4Internal.Rule, header http.Header) (url.Values, http.Header) {
    402 	query := url.Values{}
    403 	unsignedHeaders := http.Header{}
    404 	for k, h := range header {
    405 		if r.IsValid(k) {
    406 			query[k] = h
    407 		} else {
    408 			unsignedHeaders[k] = h
    409 		}
    410 	}
    411 
    412 	return query, unsignedHeaders
    413 }
    414 
    415 func (s *httpSigner) buildCanonicalHeaders(host string, rule v4Internal.Rule, header http.Header, length int64) (signed http.Header, signedHeaders, canonicalHeadersStr string) {
    416 	signed = make(http.Header)
    417 
    418 	var headers []string
    419 	const hostHeader = "host"
    420 	headers = append(headers, hostHeader)
    421 	signed[hostHeader] = append(signed[hostHeader], host)
    422 
    423 	const contentLengthHeader = "content-length"
    424 	if length > 0 {
    425 		headers = append(headers, contentLengthHeader)
    426 		signed[contentLengthHeader] = append(signed[contentLengthHeader], strconv.FormatInt(length, 10))
    427 	}
    428 
    429 	for k, v := range header {
    430 		if !rule.IsValid(k) {
    431 			continue // ignored header
    432 		}
    433 		if strings.EqualFold(k, contentLengthHeader) {
    434 			// prevent signing already handled content-length header.
    435 			continue
    436 		}
    437 
    438 		lowerCaseKey := strings.ToLower(k)
    439 		if _, ok := signed[lowerCaseKey]; ok {
    440 			// include additional values
    441 			signed[lowerCaseKey] = append(signed[lowerCaseKey], v...)
    442 			continue
    443 		}
    444 
    445 		headers = append(headers, lowerCaseKey)
    446 		signed[lowerCaseKey] = v
    447 	}
    448 	sort.Strings(headers)
    449 
    450 	signedHeaders = strings.Join(headers, ";")
    451 
    452 	var canonicalHeaders strings.Builder
    453 	n := len(headers)
    454 	const colon = ':'
    455 	for i := 0; i < n; i++ {
    456 		if headers[i] == hostHeader {
    457 			canonicalHeaders.WriteString(hostHeader)
    458 			canonicalHeaders.WriteRune(colon)
    459 			canonicalHeaders.WriteString(v4Internal.StripExcessSpaces(host))
    460 		} else {
    461 			canonicalHeaders.WriteString(headers[i])
    462 			canonicalHeaders.WriteRune(colon)
    463 			// Trim out leading, trailing, and dedup inner spaces from signed header values.
    464 			values := signed[headers[i]]
    465 			for j, v := range values {
    466 				cleanedValue := strings.TrimSpace(v4Internal.StripExcessSpaces(v))
    467 				canonicalHeaders.WriteString(cleanedValue)
    468 				if j < len(values)-1 {
    469 					canonicalHeaders.WriteRune(',')
    470 				}
    471 			}
    472 		}
    473 		canonicalHeaders.WriteRune('\n')
    474 	}
    475 	canonicalHeadersStr = canonicalHeaders.String()
    476 
    477 	return signed, signedHeaders, canonicalHeadersStr
    478 }
    479 
    480 func (s *httpSigner) buildCanonicalString(method, uri, query, signedHeaders, canonicalHeaders string) string {
    481 	return strings.Join([]string{
    482 		method,
    483 		uri,
    484 		query,
    485 		canonicalHeaders,
    486 		signedHeaders,
    487 		s.PayloadHash,
    488 	}, "\n")
    489 }
    490 
    491 func (s *httpSigner) buildStringToSign(credentialScope, canonicalRequestString string) string {
    492 	return strings.Join([]string{
    493 		signingAlgorithm,
    494 		s.Time.TimeFormat(),
    495 		credentialScope,
    496 		hex.EncodeToString(makeHash(sha256.New(), []byte(canonicalRequestString))),
    497 	}, "\n")
    498 }
    499 
    500 func makeHash(hash hash.Hash, b []byte) []byte {
    501 	hash.Reset()
    502 	hash.Write(b)
    503 	return hash.Sum(nil)
    504 }
    505 
    506 func (s *httpSigner) buildSignature(strToSign string) (string, error) {
    507 	key := s.KeyDerivator.DeriveKey(s.Credentials, s.ServiceName, s.Region, s.Time)
    508 	return hex.EncodeToString(v4Internal.HMACSHA256(key, []byte(strToSign))), nil
    509 }
    510 
    511 func (s *httpSigner) setRequiredSigningFields(headers http.Header, query url.Values) {
    512 	amzDate := s.Time.TimeFormat()
    513 
    514 	if s.IsPreSign {
    515 		query.Set(v4Internal.AmzAlgorithmKey, signingAlgorithm)
    516 		sessionToken := s.Credentials.SessionToken
    517 		if !s.DisableSessionToken && len(sessionToken) > 0 {
    518 			query.Set("X-Amz-Security-Token", sessionToken)
    519 		}
    520 
    521 		query.Set(v4Internal.AmzDateKey, amzDate)
    522 		return
    523 	}
    524 
    525 	headers[v4Internal.AmzDateKey] = append(headers[v4Internal.AmzDateKey][:0], amzDate)
    526 
    527 	if !s.DisableSessionToken && len(s.Credentials.SessionToken) > 0 {
    528 		headers[v4Internal.AmzSecurityTokenKey] = append(headers[v4Internal.AmzSecurityTokenKey][:0], s.Credentials.SessionToken)
    529 	}
    530 }
    531 
    532 func logSigningInfo(ctx context.Context, options SignerOptions, request *signedRequest, isPresign bool) {
    533 	if !options.LogSigning {
    534 		return
    535 	}
    536 	signedURLMsg := ""
    537 	if isPresign {
    538 		signedURLMsg = fmt.Sprintf(logSignedURLMsg, request.Request.URL.String())
    539 	}
    540 	logger := logging.WithContext(ctx, options.Logger)
    541 	logger.Logf(logging.Debug, logSignInfoMsg, request.CanonicalString, request.StringToSign, signedURLMsg)
    542 }
    543 
    544 type signedRequest struct {
    545 	Request         *http.Request
    546 	SignedHeaders   http.Header
    547 	CanonicalString string
    548 	StringToSign    string
    549 	PreSigned       bool
    550 }
    551 
    552 const logSignInfoMsg = `Request Signature:
    553 ---[ CANONICAL STRING  ]-----------------------------
    554 %s
    555 ---[ STRING TO SIGN ]--------------------------------
    556 %s%s
    557 -----------------------------------------------------`
    558 const logSignedURLMsg = `
    559 ---[ SIGNED URL ]------------------------------------
    560 %s`