ASVS 5.0 V11.2.5 (Level 3) requires cryptographic modules to fail securely: when decryption or verification fails, the application must refuse the data and must not reveal why through error messages, status codes or timing. The classic example is a padding oracle.
Why it matters
If your API answers "bad padding" for one kind of tampering and "bad MAC" for another, an attacker can use those answers as an oracle and decrypt data byte by byte without the key.
How to implement it
Use authenticated encryption so there is only one failure outcome, and map every crypto exception to one generic response.
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.exceptions import InvalidTag
def open_token(key: bytes, nonce: bytes, blob: bytes):
try:
return AESGCM(key).decrypt(nonce, blob, None)
except (InvalidTag, ValueError):
# one outcome for every failure; detail goes to private logs only
return None
The caller treats None as an invalid request: same status, same body, no early return that differs by cause. Never fall back to a weaker mode when decryption fails.
How to test it
Flip a bit in each part of a ciphertext (header, body, tag) and send it. Compare response bodies, status codes and timings. They should be indistinguishable.
Common mistakes
- Returning exception text to the client
- Different HTTP codes for padding and MAC errors
- Retrying with a legacy algorithm on failure
Quick check
Why use one generic failure?
Distinct errors act as an oracle that lets an attacker learn about plaintext without the key.