Preventing Signature Denial-of-Service
Authentication vs Non-Repudiation
This article is part of a series on understanding the hows and whys of JSON Web Signatures (JWS).
There's accompanying code: it's refered to and linked throughout the content. But if you'd rather just read raw code, head over here.
Signature verification is expensive. ES256 verification takes 1-5ms per request. An attacker who can force your server to verify signatures controls your CPU:
- Discover which partners your API accepts (often public information)
- Craft requests claiming to be from a legitimate partner with valid structure, fresh timestamps, and the partner's public key identifiers
- Use garbage signatures that will fail verification
- Send 10,000 requests per second
- Your server spends 10-50 CPU-seconds per second verifying invalid signatures
The pre-verification checks don't help:
- Structure? Valid (three base64url parts)
- Size? Valid (under 8KB)
- Key identifier? Valid (references a real partner)
- Timestamp? Valid (fresh
iat, futureexp) - Algorithm? Valid (
ES256)
You cannot determine the request is malicious until you perform the expensive ECDSA verification. By then, the attacker has already consumed your resources.
Rate limiting helps, but an attacker with a botnet can rotate IPs. You'll burn CPU on every unique IP until you hit your rate limit threshold.
Why JWS Can't Solve This
This is a fundamental property of asymmetric cryptography. Public-key signatures:
- Anyone can verify (public key is public)
- Only holder of private key can sign
- Non-repudiation (cryptographic proof for third parties)
- Anyone can force you to perform expensive verification
The same property that makes JWS valuable for non-repudiation (public verifiability) makes it vulnerable to DoS. There's no cryptographic primitive that provides public-key non-repudiation without this exposure.
Separating Authentication from Non-Repudiation
Authentication answers "who?" – Can this client connect?
Non-repudiation answers "what?" – What did they request, provably?
These are separate concerns requiring separate mechanisms. Payment APIs solve this by layering:
- First layer: Connection authentication
- Cost: Once per connection (amortized over many requests)
- Prevents: Unauthenticated attackers from reaching your app
- Second layer: Request authorization (cheap)
- Cost: ~100μs per request
- Prevents: Unauthorized actions by authenticated clients
- Third layer: Non-repudiation (expensive)
- Cost: ~1-5ms per request
- Prevents: Disputes about what was sent
Only authenticated, authorized clients reach the expensive signature verification layer.
How Open Banking Does This
Open Banking APIs use three layers:
Layer 1: Mutual TLS (mTLS)
- Purpose: Connection authentication
- Mechanism: Client presents eIDAS qualified certificate during TLS handshake
- Cost: Once per connection (TLS negotiation ~10-50ms, amortized over requests)
- Prevents: Unauthenticated connections
The client certificate is verified at the TLS layer before any HTTP request reaches your application. An attacker without a valid eIDAS certificate cannot establish a connection. These certificates are issued by qualified trust service providers and require regulatory approval: a significant barrier to entry.
Layer 2: OAuth 2.0 Access Token
POST /payments HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
- Purpose: Request authorization
- Mechanism: TPP exchanges client credentials for short-lived access token (5-15 min)
- Cost: ~100μs (local JWT validation or cache lookup)
- Prevents: Unauthorized API access, expired sessions
The access token is validated cheaply – either as a self-contained JWT or via cache lookup. If invalid, the request is rejected before signature verification.
Layer 3: JWS Signature
POST /payments HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
X-JWS-Signature: eyJhbGciOiJFUzI1NiIsImtpZCI6IjIwMjUtMDEtMTUifQ...
Content-Type: application/json
{"instructed_amount": {"currency": "EUR", "amount": "50000.00"}, ...}
- Purpose: Non-repudiation
- Mechanism: Detached JWS signature over request body
- Cost: ~1-5ms (ECDSA verification)
- Prevents: Disputes about payment instructions
The expensive signature verification only happens if:
- Client authenticated via mTLS (has valid eIDAS certificate)
- Access token is valid (not expired, correct scopes)
- Rate limits not exceeded
An unauthenticated attacker cannot force signature verification, because they can't establish a TLS connection. An authenticated client who abuses the API faces:
- Rate limiting per TPP
- Monitoring and alerting
- Certificate revocation
- Regulatory sanctions (license revocation)
The Architectural Principle
Expensive operations must be protected by cheap gates.
If your only authentication mechanism is JWS signature verification, you have no cheap gate. Every request, whether legitimate or malicious, forces expensive cryptographic operations.
Each of the 3 presented layers defend against different threats: remove any layer, and you're exposed.
But we've glossed over a critical operational challenge: how do you distribute and rotate the public keys used for signature verification? The answer is JWKS (JSON Web Key Sets) as we'll see in the next post: this standard mechanism turns an 8-hour emergency key rotation into a 5-minute configuration change.
This article is part of a series on understanding the hows and whys of JSON Web Signatures (JWS).
There's accompanying code: it's refered to and linked throughout the content. But if you'd rather just read raw code, head over here.