ASVS 5.0 V11.3.5 (Level 3) requires that when you combine an encryption algorithm with a separate MAC, you encrypt first and then compute the MAC over the ciphertext. Verify the MAC before you attempt to decrypt.
Why it matters
MAC then encrypt, or MAC of the plaintext, forces the receiver to decrypt before checking integrity. That opens the door to padding oracles and other chosen ciphertext attacks.
How to implement it
The best fix is to avoid the question: use an AEAD mode such as AES GCM or ChaCha20 Poly1305, which does the right thing internally. If you must compose primitives:
import hmac, hashlib
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
def seal(enc_key, mac_key, iv, padded):
ct = Cipher(algorithms.AES(enc_key), modes.CBC(iv)).encryptor()
body = iv + ct.update(padded) + ct.finalize()
tag = hmac.new(mac_key, body, hashlib.sha256).digest()
return body + tag
On receipt, split off the tag, compare with hmac.compare_digest, and only then decrypt. Use separate keys for encryption and MAC.
How to test it
Review the code path: is the MAC input the ciphertext (including IV)? Does decryption happen only after a successful compare? Tamper with one ciphertext byte and confirm no decryption is attempted.
Common mistakes
- MAC over plaintext
- Omitting the IV from the MAC input
- One key for both operations
Quick check
What is the simplest way to meet V11.3.5?
Use an AEAD cipher so the ordering is handled for you.