Skip to content
apikey.tools
Guide

Fixing AWS SignatureDoesNotMatch — A SigV4 Debugging Guide

How AWS Signature Version 4 is built step by step, how to read the canonical request AWS sends back in the error, and the ten causes behind nearly every SignatureDoesNotMatch and "signature expired" error.

By Abdessamad Bettal · Updated · 5 min read

text
SignatureDoesNotMatch: The request signature we calculated does not match the
signature you provided. Check your AWS Secret Access Key and signing method.

The error message suggests the secret key, but the secret is rarely the problem. A SigV4 signature covers the method, path, query string, a set of headers, the payload hash, the date, the region and the service. A single byte of difference in any of them produces the same unhelpful error. This guide shows how to find which one.

How SigV4 is built

Signing happens in four stages. Debugging means finding the first stage where your value differs from AWS's.

1. The canonical request

Six fields, joined by newlines:

text
GET
/my-bucket
list-type=2&prefix=photos%2F
host:s3.eu-west-1.amazonaws.com
x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-amz-date:20261024T120000Z

host;x-amz-content-sha256;x-amz-date
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

In order: HTTP method; canonical URI; canonical query string; canonical headers (lowercase names, trimmed values, sorted by name, each line ending in a newline — hence the blank line); the signed-header list; and the hex SHA-256 of the payload. The long hash above is the SHA-256 of an empty body.

2. The string to sign

text
AWS4-HMAC-SHA256
20261024T120000Z
20261024/eu-west-1/s3/aws4_request
<hex SHA-256 of the canonical request>

The third line is the credential scope: date, region, service and the literal aws4_request.

3. The signing key

The secret is never used directly. It is folded through four HMAC-SHA256 operations:

text
kDate    = HMAC("AWS4" + secretAccessKey, "20261024")
kRegion  = HMAC(kDate,    "eu-west-1")
kService = HMAC(kRegion,  "s3")
kSigning = HMAC(kService, "aws4_request")

Each step's output is raw bytes, not hex. Hex-encoding an intermediate key is a common bug in hand-written signers. You can step through it with the HMAC generator.

4. The signature and the header

text
signature = hex(HMAC(kSigning, stringToSign))

Authorization: AWS4-HMAC-SHA256 Credential=AKIA…/20261024/eu-west-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=<signature>

Read what AWS tells you

AWS often tells you exactly what it expected. S3 returns an XML error body containing <CanonicalRequest> and <StringToSign> elements — the values AWS computed. Many other services (API Gateway, STS, and others using the JSON or query protocols) include "The Canonical String for this request should have been" followed by the canonical request, and then "The String-to-Sign should have been".

Log the same two values from your own code and compare them line by line. If the canonical requests differ, the bug is in what you are signing. If the canonical requests match but the strings to sign differ, check the date and credential scope. If both match, the problem is the key itself — wrong secret, wrong region in the derivation, or whitespace in the secret.

The ten usual causes

  1. Wrong region. Signing for us-east-1 while calling an endpoint in eu-west-1. The region in the credential scope must match the endpoint. Global services such as IAM sign with us-east-1.
  2. Wrong service name. The signing name is not always the hostname: API Gateway's execute endpoint signs as execute-api, and OpenSearch Service as es.
  3. Clock skew. Requests more than five minutes away from AWS's clock fail, often as RequestTimeTooSkewed or "Signature expired". Check NTP on containers and VMs; also make sure you format the date in UTC, not local time.
  4. Path encoding. Each path segment must be URI-encoded with %20 for spaces, never +. Every service except S3 then encodes the path a *second* time when building the canonical URI; S3 encodes once. Getting this backwards breaks only keys that contain special characters, which makes it look intermittent.
  5. Query string. Parameters must be sorted by name, both names and values percent-encoded (/ becomes %2F), and a parameter with no value written as name=. The URL encoder shows strict RFC 3986 encoding.
  6. Headers changed after signing. Anything in SignedHeaders must reach AWS byte-for-byte. Proxies and HTTP clients that add, re-case or re-fold headers — or add a port to Host — invalidate the signature.
  7. Payload hash mismatch. S3 requires the x-amz-content-sha256 header, and its value must equal the hash in the canonical request. Use the literal UNSIGNED-PAYLOAD if you choose not to hash the body, and use it in both places. Check a body's hash with the hash calculator.
  8. Body re-serialised. Signing a JSON string and then letting your HTTP client serialise the object again sends different bytes than you hashed.
  9. Whitespace in the secret. A trailing newline from an environment file or secrets manager produces a valid-looking but different key.
  10. Missing session token. Temporary credentials (keys beginning ASIA) require x-amz-security-token. Its absence usually gives "The security token included in the request is invalid", but including it unsigned or after signing gives SignatureDoesNotMatch.

Prefer the SDK — and when you cannot

The AWS SDKs and the CLI sign correctly, handle clock-skew retries and pick the right service name. For hand-built requests, the CLI and cURL both have SigV4 support:

bash
# cURL 7.75+ signs requests with SigV4 itself
curl --aws-sigv4 "aws:amz:eu-west-1:execute-api" \
  --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
  -H "x-amz-security-token: $AWS_SESSION_TOKEN" \
  https://abc123.execute-api.eu-west-1.amazonaws.com/prod/items

The session token header is only needed for temporary credentials. If cURL's request succeeds where your code fails, the difference is in your canonicalisation, not your credentials.

Frequently asked questions

What does SignatureDoesNotMatch mean in AWS?

AWS recomputed the SigV4 signature from the request it received and got a different value from the one in your Authorization header or presigned URL. The secret key may be wrong, but more often a header, the path encoding, the query string, the payload hash, the region or the service name differs from what you signed.

Why does my S3 presigned URL return SignatureDoesNotMatch?

Usually because the request sent does not match the one signed: a different HTTP method (the URL was signed for GET but used with PUT), a Content-Type header that was part of the signature but sent differently, or a region mismatch between the client that signed and the bucket's actual region. Presigned URLs also expire at the time set in X-Amz-Expires.

Is SigV4 the same as HMAC-SHA256?

SigV4 uses HMAC-SHA256 throughout, but it is a full protocol on top of it: a canonical request format, a date-scoped derived key and a specific header layout. A plain HMAC of the request body is not a SigV4 signature.

What is SigV4a?

Signature Version 4A is an asymmetric variant using ECDSA P-256, designed for multi-region requests such as S3 Multi-Region Access Points. The canonical request is built the same way, but the signature is valid across a set of regions instead of one.

Try it in your browser

Every tool referenced above runs locally — nothing you paste is uploaded.

More guides