The one-line SigV4 bug that would have broken my whole feature — and the AWS CLI trick that caught it
이 글은 영어로 작성되어 있습니다.
I shipped AWS RDS IAM authentication in my mobile SQL client, DBeast. Passwordless database login: instead of a stored password, the app mints a short-lived, SigV4-signed token for rds-db:connect and hands it to the driver. I wrote the signer twice — once in JVM Kotlin for Android, once in pure Kotlin (no javax.crypto) for iOS — cross-checked them byte-for-byte against each other, watched every test go green, and almost shipped.
Both implementations were wrong. Identically wrong. Every token they would ever produce, RDS would have silently rejected.
The bug
A SigV4 canonical request ends with a hash of the request payload. For presigned S3 URLs — the SigV4 example everyone learns from — that slot holds the literal string UNSIGNED-PAYLOAD. My token generator did the same:
GET
/
Action=connect&DBUser=...
host:mydb.xxxx.us-east-1.rds.amazonaws.com:5432
host
UNSIGNED-PAYLOAD ← wrong for RDSRDS doesn't do that. generate-db-auth-token signs with the hex-encoded SHA-256 of an empty body:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855Same length rules, same shape, completely different signature. And here's the cruel part: nothing tells you. The token is just a URL with an X-Amz-Signature parameter. It's syntactically perfect. The PostgreSQL driver happily sends it as a password, RDS rejects the signature, and what you see is a generic authentication failure — indistinguishable from a wrong DBUser, a missing IAM policy, or a dozen other misconfigurations. There is no error message anywhere that says "your payload hash is wrong."
Why my tests didn't catch it
Because I made the classic crypto-testing mistake: I tested my implementations against each other. The JVM signer and the pure-Kotlin signer agreed byte-for-byte — of course they did, I wrote both from the same misreading. Self-consistency is not correctness. For anything cryptographic you need an oracle — an independent implementation you didn't write.
The trick: the AWS CLI is a free, offline oracle
The insight that saved the feature: aws rds generate-db-auth-token never contacts AWS. SigV4 is pure local computation — HMAC chains over strings. That means:
- You can diff your signer against the CLI byte-for-byte, deterministically, with no RDS instance, no network, no account setup.
- It even works with expired credentials — an old STS session token signs just fine, because nobody validates it locally. (RDS would reject the login, but the signature math is identical.)
So the parity test is embarrassingly simple: shell out to the CLI, capture its token, regenerate a token with the same hostname/port/user/credentials and the same timestamp (parse X-Amz-Date back out of the CLI's own output), and assert the X-Amz-Signature matches exactly. The test skips — not fails — when no AWS credentials are present, so CI never blocks on it.
The first run failed. Root cause: UNSIGNED-PAYLOAD. One-line fix in each signer, and the parity test has passed byte-for-byte against aws-cli 2.34.45 ever since.
Two smaller potholes from the same afternoon:
- My test had its own bug. I parsed the region out of
X-Amz-Credential(AKIA.../20260725/us-east-1/rds-db/aws4_request) at index 1 — that's the date. Region is index 2. An oracle test that's wrong is worse than no test; read your scopes carefully. - The CLI orders query parameters slightly differently than I do (
SignedHeadersvsX-Amz-Security-Tokenposition). That's cosmetic — the canonical request sorts parameters before signing, so only the signature itself has to match.
The takeaways
- Never validate a crypto implementation against your own second implementation. Two signers you wrote will share your misconceptions perfectly.
- Presigning is local — exploit that. Any AWS presigned-URL feature (RDS IAM, S3 URLs, IoT) can be oracle-tested against the CLI offline, deterministically, even with dead credentials. It's the cheapest high-confidence test you'll ever write.
- `UNSIGNED-PAYLOAD` is an S3-ism, not a SigV4-ism. When a service signs "no body", check whether it hashes the empty string instead. The docs rarely shout about it.
- Silent-failure features deserve louder tests. When the failure mode is a generic auth error with no diagnostic, your test suite is the only place the truth can surface.
RDS IAM login (and Azure Entra ID token auth) ships in DBeast — a SQL client for your phone with an incident war-room and an AI assistant that runs fully on-device. Both platforms mint tokens with the signer this post is about.