API Auth Signature Mismatch – Fix for AWS SigV4 Errors
You're calling an AWS API and getting a 403 error saying the signature doesn't match. It usually means your request headers or payload is being altered after signing.
When This Error Hits
You're building an app that talks to AWS — maybe S3, DynamoDB, or Lambda — and you fire off a signed request. Instead of getting back data, you get a 403 with SignatureDoesNotMatch. The error message looks something like:
<Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>
This usually happens during development, after you switch from AWS CLI (which handles signing automatically) to raw HTTP calls with your own code. Or when you copy a signed URL from one region to another and forget to update the endpoint. It's also common when you're using a proxy or load balancer that messes with your request headers — like adding or removing a Content-Type or Host header.
What's Actually Going Wrong
SigV4 signing is picky. You sign a specific version of your request — including the HTTP method, the URI path, query strings, headers, and the body. The AWS service then takes your request and re-computes the signature using the same algorithm. If anything — and I mean anything — is different, the signatures won't match and you get the error.
The most common culprits are:
- Timestamp drift — Your system clock is off by more than 15 minutes. AWS uses the
x-amz-dateheader (orDateheader) and compares it to server time. If they differ by over 15 minutes, the signature fails. - Wrong signing key — You're using the wrong secret access key, or you're signing with a temporary credential that's expired.
- Missing or extra headers — You signed a set of headers, but when AWS receives the request, an intermediate proxy added or removed a header. The signed headers list must match exactly.
- Payload encoding mismatch — You computed the hash of the body, but your HTTP library re-encodes the request body (like adding a charset to JSON).
- Path or query string changes — A trailing slash that wasn't in the signed request, or query parameters reordered.
The real fix is to compare the canonical request your code built against what AWS expects. I'll show you how.
Fix It Step by Step
These steps assume you're using AWS SigV4 (the default for most AWS APIs). If you're using a different auth scheme, the logic is similar — just adapt the header checks.
-
Check your system time
Run this command (Linux or macOS):
date -uOn Windows, use:
w32tm /query /statusCompare the output to the current UTC time from a trusted source like
time.is. If you're off by more than 30 seconds, sync your clock. On Linux, runsudo ntpdate pool.ntp.orgor enable NTP. On Windows, go to Date & Time settings and toggle Set time automatically.After syncing, wait a minute and retry your API call. If it works, that was the problem. If not, move on.
-
Verify your credentials
Double-check your
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEY. If you're using temporary credentials from STS, make sure they haven't expired — they typically last 15 minutes to 36 hours depending on the role. Print them from your code and compare to what's in the AWS console.Also check the region. The signing region must match the service endpoint. For example, if you're calling S3 in us-east-1, your
credentialScopemust sayus-east-1/s3/aws4_request. If you accidentally useus-west-2, the signature fails. -
Log the canonical request
This is where you'll catch 90% of mismatches. In your code, before you send the request, print or log the canonical request string — that's the string you sign. For AWS SigV4, it looks like:
POST / content-type:application/json host:s3.us-east-1.amazonaws.com x-amz-date:20250315T120000Z content-type;host;x-amz-date hash_of_bodyNow, make the same API call using the AWS CLI or SDK with debug logging turned on. For the CLI:
aws s3api list-buckets --debug 2>&1 | grep -i canonicalOr for Python SDK, set
boto3.set_stream_logger(''). Compare the canonical request from your code to the one AWS expects. Look closely at:- Line endings (must be
, no spaces) - Header order (must be sorted lowercase)
- Trailing slashes in the path
- Query string parameters (must be URL-encoded and sorted)
- Body hash (SHA-256 of the raw body, not pretty-printed JSON)
- Line endings (must be
-
Fix header issues
Common gotcha: your code signs the
Hostheader andx-amz-date, but a proxy adds or removes a header likeAccept-Encoding. If the signed headers list includeshost;x-amz-datebut the received request hasAccept-Encodingpresent, AWS doesn't care — it only cares that the headers you signed are present and match. But if your code signs all headers (usingSignedHeaders=host;x-amz-date;accept-encoding) and the proxy removesAccept-Encoding, then the signature fails.The fix: only sign the minimum required headers:
hostandx-amz-date. That's it. Don't signContent-TypeorContent-Lengthunless you have to (some services require them). Read the service's documentation. -
Check the payload hash
If your request has a body, you must include the SHA-256 hash of the body in the canonical request. Common mistake: you compute the hash of the JSON string, but then your HTTP library re-serializes it (adding or removing spaces). Make sure the body you send over the wire is exactly the same string you hashed. For binary payloads like S3 uploads, use the raw bytes.
A good test: save the exact body bytes to a file and run
sha256sum file.bin(Linux) orcertutil -hashfile file.bin SHA256(Windows). Compare that hash to the one in your canonical request. They must match. -
Use the AWS SigV4 test suite
If you're implementing signing from scratch, don't guess. AWS provides a set of test vectors. Download the SigV4 test suite. Write a small script that runs your signing function against those test cases. If your output matches the expected canonical request and signature, your algorithm is correct. If not, you'll see exactly where it goes off.
If It Still Fails
You've checked time, credentials, headers, and payload hash. Still getting the error? Here are the less common but real possibilities:
- Proxy or CDN modifies headers — If you're behind CloudFront, an API gateway, or a corporate proxy, it may strip or add headers. Test by sending the request directly to the AWS endpoint (bypass the proxy) using
curlfrom a machine outside your network. - URL encoding mismatch — AWS expects query parameter values to be encoded with percent-encoding, and the signing algorithm uses a specific set of characters to encode. For example, a space must be
%20, not+. Check your URL encoding library. I've seen Python'surllib.parse.quotevsrequestsproduce different results. - Empty body handling — For GET requests, the body hash should be the hash of an empty string (
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855). Some implementations mistakenly skip the body hash entirely. - Region or service name typo —
s3vsS3(case matters in the credential scope). Double-check the service name in the AWS documentation for that specific endpoint. - Signature version mismatch — Make sure you're using
aws4_requestin the credential scope, not a previous version likeaws2_request.
If none of that works, take a step back. Use an AWS SDK for your language — Python's boto3, JavaScript's AWS SDK, or the Go SDK v2. They handle signing perfectly. Then compare the signed request your SDK sends to the one your custom code generates. The SDK logs will show you the exact canonical request. Copy that into your code. That's the fastest path to a fix.
Was this solution helpful?