src

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

v4a.go (15079B)


      1 package v4a
      2 
      3 import (
      4 	"bytes"
      5 	"context"
      6 	"crypto"
      7 	"crypto/ecdsa"
      8 	"crypto/elliptic"
      9 	"crypto/rand"
     10 	"crypto/sha256"
     11 	"encoding/hex"
     12 	"fmt"
     13 	"hash"
     14 	"math/big"
     15 	"net/http"
     16 	"net/textproto"
     17 	"net/url"
     18 	"sort"
     19 	"strconv"
     20 	"strings"
     21 	"time"
     22 
     23 	signerCrypto "github.com/aws/aws-sdk-go-v2/internal/v4a/internal/crypto"
     24 	v4Internal "github.com/aws/aws-sdk-go-v2/internal/v4a/internal/v4"
     25 	"github.com/aws/smithy-go/encoding/httpbinding"
     26 	"github.com/aws/smithy-go/logging"
     27 )
     28 
     29 const (
     30 	// AmzRegionSetKey represents the region set header used for sigv4a
     31 	AmzRegionSetKey     = "X-Amz-Region-Set"
     32 	amzAlgorithmKey     = v4Internal.AmzAlgorithmKey
     33 	amzSecurityTokenKey = v4Internal.AmzSecurityTokenKey
     34 	amzDateKey          = v4Internal.AmzDateKey
     35 	amzCredentialKey    = v4Internal.AmzCredentialKey
     36 	amzSignedHeadersKey = v4Internal.AmzSignedHeadersKey
     37 	authorizationHeader = "Authorization"
     38 
     39 	signingAlgorithm = "AWS4-ECDSA-P256-SHA256"
     40 
     41 	timeFormat      = "20060102T150405Z"
     42 	shortTimeFormat = "20060102"
     43 
     44 	// EmptyStringSHA256 is a hex encoded SHA-256 hash of an empty string
     45 	EmptyStringSHA256 = v4Internal.EmptyStringSHA256
     46 
     47 	// Version of signing v4a
     48 	Version = "SigV4A"
     49 )
     50 
     51 var (
     52 	p256          elliptic.Curve
     53 	nMinusTwoP256 *big.Int
     54 
     55 	one = new(big.Int).SetInt64(1)
     56 )
     57 
     58 func init() {
     59 	// Ensure the elliptic curve parameters are initialized on package import rather then on first usage
     60 	p256 = elliptic.P256()
     61 
     62 	nMinusTwoP256 = new(big.Int).SetBytes(p256.Params().N.Bytes())
     63 	nMinusTwoP256 = nMinusTwoP256.Sub(nMinusTwoP256, new(big.Int).SetInt64(2))
     64 }
     65 
     66 // SignerOptions is the SigV4a signing options for constructing a Signer.
     67 type SignerOptions struct {
     68 	Logger     logging.Logger
     69 	LogSigning bool
     70 
     71 	// Disables the Signer's moving HTTP header key/value pairs from the HTTP
     72 	// request header to the request's query string. This is most commonly used
     73 	// with pre-signed requests preventing headers from being added to the
     74 	// request's query string.
     75 	DisableHeaderHoisting bool
     76 
     77 	// Disables the automatic escaping of the URI path of the request for the
     78 	// siganture's canonical string's path. For services that do not need additional
     79 	// escaping then use this to disable the signer escaping the path.
     80 	//
     81 	// S3 is an example of a service that does not need additional escaping.
     82 	//
     83 	// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
     84 	DisableURIPathEscaping bool
     85 }
     86 
     87 // Signer is a SigV4a HTTP signing implementation
     88 type Signer struct {
     89 	options SignerOptions
     90 }
     91 
     92 // NewSigner constructs a SigV4a Signer.
     93 func NewSigner(optFns ...func(*SignerOptions)) *Signer {
     94 	options := SignerOptions{}
     95 
     96 	for _, fn := range optFns {
     97 		fn(&options)
     98 	}
     99 
    100 	return &Signer{options: options}
    101 }
    102 
    103 // deriveKeyFromAccessKeyPair derives a NIST P-256 PrivateKey from the given
    104 // IAM AccessKey and SecretKey pair.
    105 //
    106 // Based on FIPS.186-4 Appendix B.4.2
    107 func deriveKeyFromAccessKeyPair(accessKey, secretKey string) (*ecdsa.PrivateKey, error) {
    108 	params := p256.Params()
    109 	bitLen := params.BitSize // Testing random candidates does not require an additional 64 bits
    110 	counter := 0x01
    111 
    112 	buffer := make([]byte, 1+len(accessKey)) // 1 byte counter + len(accessKey)
    113 	kdfContext := bytes.NewBuffer(buffer)
    114 
    115 	inputKey := append([]byte("AWS4A"), []byte(secretKey)...)
    116 
    117 	d := new(big.Int)
    118 	for {
    119 		kdfContext.Reset()
    120 		kdfContext.WriteString(accessKey)
    121 		kdfContext.WriteByte(byte(counter))
    122 
    123 		key, err := signerCrypto.HMACKeyDerivation(sha256.New, bitLen, inputKey, []byte(signingAlgorithm), kdfContext.Bytes())
    124 		if err != nil {
    125 			return nil, err
    126 		}
    127 
    128 		// Check key first before calling SetBytes if key key is in fact a valid candidate.
    129 		// This ensures the byte slice is the correct length (32-bytes) to compare in constant-time
    130 		cmp, err := signerCrypto.ConstantTimeByteCompare(key, nMinusTwoP256.Bytes())
    131 		if err != nil {
    132 			return nil, err
    133 		}
    134 		if cmp == -1 {
    135 			d.SetBytes(key)
    136 			break
    137 		}
    138 
    139 		counter++
    140 		if counter > 0xFF {
    141 			return nil, fmt.Errorf("exhausted single byte external counter")
    142 		}
    143 	}
    144 	d = d.Add(d, one)
    145 
    146 	priv := new(ecdsa.PrivateKey)
    147 	priv.PublicKey.Curve = p256
    148 	priv.D = d
    149 	priv.PublicKey.X, priv.PublicKey.Y = p256.ScalarBaseMult(d.Bytes())
    150 
    151 	return priv, nil
    152 }
    153 
    154 type httpSigner struct {
    155 	Request     *http.Request
    156 	ServiceName string
    157 	RegionSet   []string
    158 	Time        time.Time
    159 	Credentials Credentials
    160 	IsPreSign   bool
    161 
    162 	Logger logging.Logger
    163 	Debug  bool
    164 
    165 	// PayloadHash is the hex encoded SHA-256 hash of the request payload
    166 	// If len(PayloadHash) == 0 the signer will attempt to send the request
    167 	// as an unsigned payload. Note: Unsigned payloads only work for a subset of services.
    168 	PayloadHash string
    169 
    170 	DisableHeaderHoisting  bool
    171 	DisableURIPathEscaping bool
    172 }
    173 
    174 // SignHTTP takes the provided http.Request, payload hash, service, regionSet, and time and signs using SigV4a.
    175 // The passed in request will be modified in place.
    176 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 {
    177 	options := s.options
    178 	for _, fn := range optFns {
    179 		fn(&options)
    180 	}
    181 
    182 	signer := &httpSigner{
    183 		Request:                r,
    184 		PayloadHash:            payloadHash,
    185 		ServiceName:            service,
    186 		RegionSet:              regionSet,
    187 		Credentials:            credentials,
    188 		Time:                   signingTime.UTC(),
    189 		DisableHeaderHoisting:  options.DisableHeaderHoisting,
    190 		DisableURIPathEscaping: options.DisableURIPathEscaping,
    191 	}
    192 
    193 	signedRequest, err := signer.Build()
    194 	if err != nil {
    195 		return err
    196 	}
    197 
    198 	logHTTPSigningInfo(ctx, options, signedRequest)
    199 
    200 	return nil
    201 }
    202 
    203 // PresignHTTP takes the provided http.Request, payload hash, service, regionSet, and time and presigns using SigV4a
    204 // Returns the presigned URL along with the headers that were signed with the request.
    205 //
    206 // PresignHTTP will not set the expires time of the presigned request
    207 // automatically. To specify the expire duration for a request add the
    208 // "X-Amz-Expires" query parameter on the request with the value as the
    209 // duration in seconds the presigned URL should be considered valid for. This
    210 // parameter is not used by all AWS services, and is most notable used by
    211 // Amazon S3 APIs.
    212 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) {
    213 	options := s.options
    214 	for _, fn := range optFns {
    215 		fn(&options)
    216 	}
    217 
    218 	signer := &httpSigner{
    219 		Request:                r,
    220 		PayloadHash:            payloadHash,
    221 		ServiceName:            service,
    222 		RegionSet:              regionSet,
    223 		Credentials:            credentials,
    224 		Time:                   signingTime.UTC(),
    225 		IsPreSign:              true,
    226 		DisableHeaderHoisting:  options.DisableHeaderHoisting,
    227 		DisableURIPathEscaping: options.DisableURIPathEscaping,
    228 	}
    229 
    230 	signedRequest, err := signer.Build()
    231 	if err != nil {
    232 		return "", nil, err
    233 	}
    234 
    235 	logHTTPSigningInfo(ctx, options, signedRequest)
    236 
    237 	signedHeaders = make(http.Header)
    238 
    239 	// For the signed headers we canonicalize the header keys in the returned map.
    240 	// This avoids situations where can standard library double headers like host header. For example the standard
    241 	// library will set the Host header, even if it is present in lower-case form.
    242 	for k, v := range signedRequest.SignedHeaders {
    243 		key := textproto.CanonicalMIMEHeaderKey(k)
    244 		signedHeaders[key] = append(signedHeaders[key], v...)
    245 	}
    246 
    247 	return signedRequest.Request.URL.String(), signedHeaders, nil
    248 }
    249 
    250 func (s *httpSigner) setRequiredSigningFields(headers http.Header, query url.Values) {
    251 	amzDate := s.Time.Format(timeFormat)
    252 
    253 	if s.IsPreSign {
    254 		query.Set(AmzRegionSetKey, strings.Join(s.RegionSet, ","))
    255 		query.Set(amzDateKey, amzDate)
    256 		query.Set(amzAlgorithmKey, signingAlgorithm)
    257 		if len(s.Credentials.SessionToken) > 0 {
    258 			query.Set(amzSecurityTokenKey, s.Credentials.SessionToken)
    259 		}
    260 		return
    261 	}
    262 
    263 	headers.Set(AmzRegionSetKey, strings.Join(s.RegionSet, ","))
    264 	headers.Set(amzDateKey, amzDate)
    265 	if len(s.Credentials.SessionToken) > 0 {
    266 		headers.Set(amzSecurityTokenKey, s.Credentials.SessionToken)
    267 	}
    268 }
    269 
    270 func (s *httpSigner) Build() (signedRequest, error) {
    271 	req := s.Request
    272 
    273 	query := req.URL.Query()
    274 	headers := req.Header
    275 
    276 	s.setRequiredSigningFields(headers, query)
    277 
    278 	// Sort Each Query Key's Values
    279 	for key := range query {
    280 		sort.Strings(query[key])
    281 	}
    282 
    283 	v4Internal.SanitizeHostForHeader(req)
    284 
    285 	credentialScope := s.buildCredentialScope()
    286 	credentialStr := s.Credentials.Context + "/" + credentialScope
    287 	if s.IsPreSign {
    288 		query.Set(amzCredentialKey, credentialStr)
    289 	}
    290 
    291 	unsignedHeaders := headers
    292 	if s.IsPreSign && !s.DisableHeaderHoisting {
    293 		urlValues := url.Values{}
    294 		urlValues, unsignedHeaders = buildQuery(v4Internal.AllowedQueryHoisting, unsignedHeaders)
    295 		for k := range urlValues {
    296 			query[k] = urlValues[k]
    297 		}
    298 	}
    299 
    300 	host := req.URL.Host
    301 	if len(req.Host) > 0 {
    302 		host = req.Host
    303 	}
    304 
    305 	signedHeaders, signedHeadersStr, canonicalHeaderStr := s.buildCanonicalHeaders(host, v4Internal.IgnoredHeaders, unsignedHeaders, s.Request.ContentLength)
    306 
    307 	if s.IsPreSign {
    308 		query.Set(amzSignedHeadersKey, signedHeadersStr)
    309 	}
    310 
    311 	rawQuery := strings.Replace(query.Encode(), "+", "%20", -1)
    312 
    313 	canonicalURI := v4Internal.GetURIPath(req.URL)
    314 	if !s.DisableURIPathEscaping {
    315 		canonicalURI = httpbinding.EscapePath(canonicalURI, false)
    316 	}
    317 
    318 	canonicalString := s.buildCanonicalString(
    319 		req.Method,
    320 		canonicalURI,
    321 		rawQuery,
    322 		signedHeadersStr,
    323 		canonicalHeaderStr,
    324 	)
    325 
    326 	strToSign := s.buildStringToSign(credentialScope, canonicalString)
    327 	signingSignature, err := s.buildSignature(strToSign)
    328 	if err != nil {
    329 		return signedRequest{}, err
    330 	}
    331 
    332 	if s.IsPreSign {
    333 		rawQuery += "&X-Amz-Signature=" + signingSignature
    334 	} else {
    335 		headers[authorizationHeader] = append(headers[authorizationHeader][:0], buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature))
    336 	}
    337 
    338 	req.URL.RawQuery = rawQuery
    339 
    340 	return signedRequest{
    341 		Request:         req,
    342 		SignedHeaders:   signedHeaders,
    343 		CanonicalString: canonicalString,
    344 		StringToSign:    strToSign,
    345 		PreSigned:       s.IsPreSign,
    346 	}, nil
    347 }
    348 
    349 func buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature string) string {
    350 	const credential = "Credential="
    351 	const signedHeaders = "SignedHeaders="
    352 	const signature = "Signature="
    353 	const commaSpace = ", "
    354 
    355 	var parts strings.Builder
    356 	parts.Grow(len(signingAlgorithm) + 1 +
    357 		len(credential) + len(credentialStr) + len(commaSpace) +
    358 		len(signedHeaders) + len(signedHeadersStr) + len(commaSpace) +
    359 		len(signature) + len(signingSignature),
    360 	)
    361 	parts.WriteString(signingAlgorithm)
    362 	parts.WriteRune(' ')
    363 	parts.WriteString(credential)
    364 	parts.WriteString(credentialStr)
    365 	parts.WriteString(commaSpace)
    366 	parts.WriteString(signedHeaders)
    367 	parts.WriteString(signedHeadersStr)
    368 	parts.WriteString(commaSpace)
    369 	parts.WriteString(signature)
    370 	parts.WriteString(signingSignature)
    371 	return parts.String()
    372 }
    373 
    374 func (s *httpSigner) buildCredentialScope() string {
    375 	return strings.Join([]string{
    376 		s.Time.Format(shortTimeFormat),
    377 		s.ServiceName,
    378 		"aws4_request",
    379 	}, "/")
    380 
    381 }
    382 
    383 func buildQuery(r v4Internal.Rule, header http.Header) (url.Values, http.Header) {
    384 	query := url.Values{}
    385 	unsignedHeaders := http.Header{}
    386 	for k, h := range header {
    387 		if r.IsValid(k) {
    388 			query[k] = h
    389 		} else {
    390 			unsignedHeaders[k] = h
    391 		}
    392 	}
    393 
    394 	return query, unsignedHeaders
    395 }
    396 
    397 func (s *httpSigner) buildCanonicalHeaders(host string, rule v4Internal.Rule, header http.Header, length int64) (signed http.Header, signedHeaders, canonicalHeadersStr string) {
    398 	signed = make(http.Header)
    399 
    400 	var headers []string
    401 	const hostHeader = "host"
    402 	headers = append(headers, hostHeader)
    403 	signed[hostHeader] = append(signed[hostHeader], host)
    404 
    405 	if length > 0 {
    406 		const contentLengthHeader = "content-length"
    407 		headers = append(headers, contentLengthHeader)
    408 		signed[contentLengthHeader] = append(signed[contentLengthHeader], strconv.FormatInt(length, 10))
    409 	}
    410 
    411 	for k, v := range header {
    412 		if !rule.IsValid(k) {
    413 			continue // ignored header
    414 		}
    415 
    416 		lowerCaseKey := strings.ToLower(k)
    417 		if _, ok := signed[lowerCaseKey]; ok {
    418 			// include additional values
    419 			signed[lowerCaseKey] = append(signed[lowerCaseKey], v...)
    420 			continue
    421 		}
    422 
    423 		headers = append(headers, lowerCaseKey)
    424 		signed[lowerCaseKey] = v
    425 	}
    426 	sort.Strings(headers)
    427 
    428 	signedHeaders = strings.Join(headers, ";")
    429 
    430 	var canonicalHeaders strings.Builder
    431 	n := len(headers)
    432 	const colon = ':'
    433 	for i := range n {
    434 		if headers[i] == hostHeader {
    435 			canonicalHeaders.WriteString(hostHeader)
    436 			canonicalHeaders.WriteRune(colon)
    437 			canonicalHeaders.WriteString(v4Internal.StripExcessSpaces(host))
    438 		} else {
    439 			canonicalHeaders.WriteString(headers[i])
    440 			canonicalHeaders.WriteRune(colon)
    441 			// Trim out leading, trailing, and dedup inner spaces from signed header values.
    442 			values := signed[headers[i]]
    443 			for j, v := range values {
    444 				cleanedValue := strings.TrimSpace(v4Internal.StripExcessSpaces(v))
    445 				canonicalHeaders.WriteString(cleanedValue)
    446 				if j < len(values)-1 {
    447 					canonicalHeaders.WriteRune(',')
    448 				}
    449 			}
    450 		}
    451 		canonicalHeaders.WriteRune('\n')
    452 	}
    453 	canonicalHeadersStr = canonicalHeaders.String()
    454 
    455 	return signed, signedHeaders, canonicalHeadersStr
    456 }
    457 
    458 func (s *httpSigner) buildCanonicalString(method, uri, query, signedHeaders, canonicalHeaders string) string {
    459 	return strings.Join([]string{
    460 		method,
    461 		uri,
    462 		query,
    463 		canonicalHeaders,
    464 		signedHeaders,
    465 		s.PayloadHash,
    466 	}, "\n")
    467 }
    468 
    469 func (s *httpSigner) buildStringToSign(credentialScope, canonicalRequestString string) string {
    470 	return strings.Join([]string{
    471 		signingAlgorithm,
    472 		s.Time.Format(timeFormat),
    473 		credentialScope,
    474 		hex.EncodeToString(makeHash(sha256.New(), []byte(canonicalRequestString))),
    475 	}, "\n")
    476 }
    477 
    478 func makeHash(hash hash.Hash, b []byte) []byte {
    479 	hash.Reset()
    480 	hash.Write(b)
    481 	return hash.Sum(nil)
    482 }
    483 
    484 func (s *httpSigner) buildSignature(strToSign string) (string, error) {
    485 	sig, err := s.Credentials.PrivateKey.Sign(rand.Reader, makeHash(sha256.New(), []byte(strToSign)), crypto.SHA256)
    486 	if err != nil {
    487 		return "", err
    488 	}
    489 	return hex.EncodeToString(sig), nil
    490 }
    491 
    492 const logSignInfoMsg = `Request Signature:
    493 ---[ CANONICAL STRING  ]-----------------------------
    494 %s
    495 ---[ STRING TO SIGN ]--------------------------------
    496 %s%s
    497 -----------------------------------------------------`
    498 const logSignedURLMsg = `
    499 ---[ SIGNED URL ]------------------------------------
    500 %s`
    501 
    502 func logHTTPSigningInfo(ctx context.Context, options SignerOptions, r signedRequest) {
    503 	if !options.LogSigning {
    504 		return
    505 	}
    506 	signedURLMsg := ""
    507 	if r.PreSigned {
    508 		signedURLMsg = fmt.Sprintf(logSignedURLMsg, r.Request.URL.String())
    509 	}
    510 	logger := logging.WithContext(ctx, options.Logger)
    511 	logger.Logf(logging.Debug, logSignInfoMsg, r.CanonicalString, r.StringToSign, signedURLMsg)
    512 }
    513 
    514 type signedRequest struct {
    515 	Request         *http.Request
    516 	SignedHeaders   http.Header
    517 	CanonicalString string
    518 	StringToSign    string
    519 	PreSigned       bool
    520 }