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`