How does HMAC request signing work, what must a canonical string include, and how do you verify a signature safely?
Predict first
What do the signatures show?
import Foundation
import CryptoKit
let key = SymmetricKey(data: Data("secret".utf8))
func sign(method: String, path: String, body: Data, timestamp: String) -> String {
let bodyHash = SHA256.hash(data: body).map { String(format: "%02x", $0) }.joined()
let canonical = [method, path, timestamp, bodyHash].joined(separator: "\n")
let mac = HMAC<SHA256>.authenticationCode(for: Data(canonical.utf8), using: key)
return Data(mac).map { String(format: "%02x", $0) }.joined()
}
let a = sign(method: "POST", path: "/pay", body: Data("{\"amount\":5}".utf8), timestamp: "1700000000")
let b = sign(method: "POST", path: "/pay", body: Data("{\"amount\":5}".utf8), timestamp: "1700000000")
let c = sign(method: "POST", path: "/pay", body: Data("{\"amount\":6}".utf8), timestamp: "1700000000")
print("same input same signature:", a == b, "changed body changes it:", a != c, "length:", a.count)
let canonical = Data("POST\n/pay\n1700000000".utf8)
let mac = HMAC<SHA256>.authenticationCode(for: canonical, using: key)
print("verify ok:", HMAC<SHA256>.isValidAuthenticationCode(mac, authenticating: canonical, using: key),
"verify tampered:", HMAC<SHA256>.isValidAuthenticationCode(mac, authenticating: Data("POST\n/pay\n1700000001".utf8), using: key))
- same input same signature: true changed body changes it: true length: 64 / verify ok: true verify tampered: false
- same input same signature: false …
- length: 32
Mental model. A wax seal with a stamp only the sender and receiver hold: any change to the letter or its date breaks the seal, and a very old sealed letter is rejected by its date.
When an API must know a request was not altered and came from a client holding a shared secret, the client sends a signature: an HMAC (here HMAC-SHA256) over a canonical string built from the parts of the request that matter. A good canonical string includes the method, path (and query), a timestamp, and a hash of the body (joined with newlines, in a fixed order), so changing any of them changes the signature. The check shows the properties you rely on: the same input gives the same signature, a changed body (amount 5 to 6) gives a different one, and the output is 64 hex characters (32 bytes). The server recomputes the signature from what it received and compares; the timestamp (checked against a small window) and often a nonce stop an attacker replaying an old, valid request.
Verify with isValidAuthenticationCode, which compares in constant time, never with == on strings, which can leak how many leading characters matched. The honest limits: a secret embedded in a shipped app can be extracted, so a client-side shared secret authenticates the app build weakly at best; use it for integrity and replay protection, not as a user credential; per-user or per-device keys derived at login are stronger; and asymmetric signatures (the server holds only the public key) or App Attest give real device attestation. Keep clocks in mind (send the server time offset), define the canonical form exactly (case, header lists, encoding), and rotate keys with a key id in the request.
HMAC is deterministic, any change alters it, and verification fails for tampered input.
Common traps
- Comparing signatures with
==. It is not constant-time; useisValidAuthenticationCode. - Treating an embedded secret as secret. It can be extracted from the binary; it gives integrity, not strong identity.
They might follow up with
- Why include a timestamp? To limit replay; the server rejects requests outside a short window and can track nonces.
- What gives stronger proof of a genuine app? App Attest or DeviceCheck, combined with server-side checks (Q344).
Related questions
- 12.9.3 What does a multipart/form-data body look like on the wire, and what are the boundary, CRLF and Content-Type…
- 12.9.1 Five requests fail with 401 at the same moment. How do you refresh the token once, retry each request once…
- 12.2.2 How do TLS and App Transport Security protect a request, and what is certificate pinning, and why is it…
- 14.2.2 What do App Attest and DeviceCheck prove, when do they help, and what are their limits?
← What does a multipart/form-data body look like on the wire, and what…