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:
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
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855In 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
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:
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
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
- Wrong region. Signing for
us-east-1while calling an endpoint ineu-west-1. The region in the credential scope must match the endpoint. Global services such as IAM sign withus-east-1. - Wrong service name. The signing name is not always the hostname: API Gateway's execute endpoint signs as
execute-api, and OpenSearch Service ases. - Clock skew. Requests more than five minutes away from AWS's clock fail, often as
RequestTimeTooSkewedor "Signature expired". Check NTP on containers and VMs; also make sure you format the date in UTC, not local time. - Path encoding. Each path segment must be URI-encoded with
%20for 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. - Query string. Parameters must be sorted by name, both names and values percent-encoded (
/becomes%2F), and a parameter with no value written asname=. The URL encoder shows strict RFC 3986 encoding. - Headers changed after signing. Anything in
SignedHeadersmust reach AWS byte-for-byte. Proxies and HTTP clients that add, re-case or re-fold headers — or add a port toHost— invalidate the signature. - Payload hash mismatch. S3 requires the
x-amz-content-sha256header, and its value must equal the hash in the canonical request. Use the literalUNSIGNED-PAYLOADif you choose not to hash the body, and use it in both places. Check a body's hash with the hash calculator. - Body re-serialised. Signing a JSON string and then letting your HTTP client serialise the object again sends different bytes than you hashed.
- Whitespace in the secret. A trailing newline from an environment file or secrets manager produces a valid-looking but different key.
- Missing session token. Temporary credentials (keys beginning
ASIA) requirex-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:
# 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/itemsThe 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.