1. The two sides
AWS's IAM troubleshooting guide says: "Compare the canonical request and string to your signed request with the value in the error message. Modify the signing process if there are any differences." Load an example or paste your own. Nothing leaves this page; still, consider redacting access key IDs before pasting anything anywhere.
2. Line by line
3. The rule catalogue
Rules K1 to K11 and K13 to K15 come from the IAM User Guide page "Create a signed AWS API request" and its troubleshooting page. K12 comes from the documentation in the AWS SDK for JavaScript signer (smithy-typescript, SignatureV4Base.ts), not from AWS service documentation. Anything that matches no rule is reported as "differs; no catalogued rule matched", never as a guess.
- K1
- "The space character is a reserved character and must be encoded as "%20" (and not as "+")."
- K2
- "Letters in the hexadecimal value must be uppercase, for example "%1A"."
- K3
- An unreserved character (A to Z, a to z, 0 to 9, hyphen, period, underscore, tilde) was percent-encoded, or a reserved one was left raw.
- K4
- "You must also sort the parameters in the canonical query string alphabetically by key name. The sorting occurs after encoding." Mixed case keys are compared by byte value here.
- K5
- A parameter without a value still needs its "=": the IAM page's example for ?acl is UriEncode("acl") + "=" + "".
- K6
- Header names must be lowercase and the headers sorted by name.
- K7
- Header values: "trim any leading or trailing spaces. convert sequential spaces to a single space."
- K8
- "separate the values for a multi-value header using commas."
- K9
- The SignedHeaders line must list exactly the canonical header names, and both sides must sign the same set.
- K10
- A signed header has a value on one side and is empty on the other: the header arrived empty or different, so something between the client and AWS changed it. The troubleshooting page: "You can also verify that you didn't send the request through a proxy that modifies the headers or the request."
- K11
- "If there is no payload in the request, you compute a hash of the empty string", which is e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855. "For Amazon S3, include the literal string UNSIGNED-PAYLOAD when constructing a canonical request, and set the same value as the x-amz-content-sha256 header value when sending the request."
- K12
- Path encoded once on one side and twice on the other (%25XX against %XX), or normalized on one side only. The SDK signer documents path escaping as "required for every AWS service, except Amazon S3, as of late 2017", says "Non-S3 services, we normalize the path and then double URI encode it.", and for S3 that "we shouldn't normalize the path".
- K13
- "Encode the forward slash character, '/', everywhere except in the object key name."
- K14
- A volatile header was signed. The IAM page lists headers not to sign, "including connection , x-amzn-trace-id , user-agent , keep-alive , transfer-encoding , TE , trailer , upgrade , proxy-authorization , and proxy-authenticate". A warning, not an error.
- K15
- A required header is not signed: "You must include the host header (HTTP/1.1) or the :authority header (HTTP/2), and any x-amz-* headers in the signature." and "The x-amz-content-sha256 header is required for Amazon S3 AWS requests."
- K16
- The last line of a string to sign must be the SHA-256 hex of that side's canonical request. A mismatch on your side means your signer hashed something other than what you pasted.
S3 error bodies commonly include CanonicalRequest and StringToSign elements; not every AWS service returns the canonical request. SigV4a (AWS4-ECDSA-P256-SHA256) is recognised, but its credential scope rule differs and it is not checked. This is an independent tool, not affiliated with or endorsed by Amazon Web Services.
4. Self test
One fixture per rule K1 to K16, the eight presets, the empty-string hash recomputed live with WebCrypto, and a fuzz test: random canonical requests from a fixed seed, each mutated by exactly one catalogued rule, where the differ must name that rule. The last row is a positive control written to fail; if it ever holds, the suite is not really running.
Running...