code.dwrz.net

Go monorepo.
Log | Files | Refs

v4.go (17958B)


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