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 }