ASVS 5.0 V4.1.5 (level 3) asks for per message digital signatures, on top of transport protection, for requests or transactions that are highly sensitive or that pass through several systems.
TLS protects a single connection. If a payment instruction passes through a gateway, a queue and two internal services, each hop terminates TLS and could read or change the message. A signature made by the original sender travels with the message and proves it was not altered.
Why it matters
Hop by hop encryption does not give end to end integrity or proof of origin. Intermediaries, compromised internal services and replay attacks sit in that gap. For money movement, regulatory filings, webhooks and cross organization APIs, you want the receiver to verify who sent this exact payload.
This is a level 3 item because it adds real cost: key management, clock handling and replay defense. Use it where the risk justifies it.
How to implement it
Sign a canonical string made of the method, path, a timestamp, a unique id and a digest of the body. HTTP Message Signatures (RFC 9421) standardizes this, and a simple HMAC or asymmetric scheme works for private APIs.
A small Node example using an Ed25519 key:
import { sign, verify, createHash } from 'node:crypto';
function canonical(req, body, ts, nonce) {
const digest = createHash('sha256').update(body).digest('base64');
return [req.method, req.path, ts, nonce, digest].join('\n');
}
// sender
const sig = sign(null, Buffer.from(canonical(req, body, ts, nonce)), privateKey);
// receiver
const ok = verify(null, Buffer.from(canonical(req, body, ts, nonce)), publicKey, sig);
const fresh = Math.abs(Date.now() - Number(ts)) < 300000;
if (!ok || !fresh || await nonceSeen(nonce)) reject();
Store seen nonces for the freshness window, rotate keys with a key id in the message, and keep private keys in a KMS or HSM.
How to test it
Take a valid signed request and change one thing at a time: the body, the path, the timestamp, the nonce. Each variant must be rejected. Then replay the untouched request a second time inside the freshness window. The second copy must fail on the nonce check. Finally send a request signed with a retired key id and confirm it is refused.
Common mistakes
- Signing only the body and leaving the path, method or timestamp unprotected.
- No replay defense, so a captured valid request works forever.
- Comparing signatures with a plain string equality in HMAC schemes. Use a constant time compare.
- Sharing one symmetric key across every partner.
- Verifying in the gateway but forwarding unsigned messages to services that trust them.
Quick check
Why is TLS not enough for a request that crosses several services?
Each hop ends the TLS session, so any hop can read or change the message. A signature from the original sender lets the final receiver verify the exact payload.
Sources
- OWASP Application Security Verification Standard, requirement V4.1.5 (level 3)
- RFC 9421, HTTP Message Signatures