Make cbc_pad its own mode
Currently, #11825 fixed the CBC_PAD implementation for AES. However, it would be useful to make
cbc_pad its own mode (like cbc, ecb, etc) to allow other mechanisms (e.g. DES3 for those unfortunate enough to have to still use it) to also make use of it. Additionally we can improve the padding validation currently being done to be more resistant to timing and oracle attacks.
Updated by Jason King over 1 year ago
A bit of history for the timing improvements:
Originally, the PKCS5/7 padding (in the context of AES they're identical), would examine the last byte of the decrypted cipher text, and work backwards, returning an error if on any padding byte that was invalid (i.e. wasn't equal to the amount of padding added).
Since the amount of work is proportional to the amount of padding, this could potentially lead to exploiting the timing as part of a padding oracle attack. #11825 improved on this by checking each padding byte and setting an error flag each time an invalid padding byte was found. This was better since it made it more difficult to determine which byte was bad, but still suffered from potentially leaking the amount of padding via timing.
Since making the cbc_pad code it's own mode will require redoing the padding validation to not be so tied to PCKS#11, we can further improve the validation here to be more resistant to timing attacks. We can use bit-wise operations to mask out the non-padding bits (so non-padding bits result in 0 when doing the validation), then bitwise-XOR the padding bits with the expected value (so non-matches result in a non-zero result), doing a bitwise-OR against the result of each byte of the final block of decrypted ciphertext. Any non-zero values will indicate a padding error, but does so in as close to constant time as we can get since it always iterates over the entire final block of cipher text, always does the same operations on each byte -- the only difference is the choice of mask value (an index into a two-byte array) based on if the current byte is part of the padding or not (I'm not sure there's a way to avoid that).
The common code will need to support the error semantics of PKCS#11, which appear to be compatible with KCF (unfortunately, most of the KCF design documentation does not appear to be publicly available, but a comparison of the APIs strongly suggests that the KCF was modeled after PKCS#11). Specifically:
- An invalid cipertext input length (not the decrypted cipher text length) returns CKR_DATA_LEN_RANGE (KCF equivalent is CRYPTO_DATA_LEN_RANGE) and fails the operation. This does not render itself vulnerable to a padding oracle attack since the mechanism itself mandates that during decryption, the input cipher text must be a multiple of the mechanism block size, so an observer seeing cipher text that isn't a multiple of the cipher block size already knows it's invalid without having to examine the data.
- A caller may pass NULL for the location of the output buffer when performing encryption or decryption to query PKCS#11 on the amount of space required. This does not terminate the current crypto operation (these functions pass a pointer to the size of the output buffer for the crypto op, and PKCS#11 will set the value in this pointer when queried for the output size). PKCS#11 is allowed only in this instance to estimate (i.e. it can round up).
- When encrypting, the output cipher text is rounded up to the next multiple of the cipher block size (even if the input plaintext is already a multiple of the cipher block size -- in this instance a full block of padding is added). This derives from the definition of PKCS7 padding, and the caller must either supply a sufficiently large buffer when doing encryption as a single-part operation, or must provide enough output space over the course of the C_EncryptUpdate()/C_EncryptFinal() calls to hold that much data.
- When decrypting, if the output buffer is too small, CKR_BUFFER_TOO_SMALL is returned to the caller, and the exact amount of space required is set in the size pointer passed to the C_Decrypt()/C_DecryptFinal() call.