Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CipherCtx::cipher_update is too pessimistic #1729

Closed
wiktor-k opened this issue Nov 9, 2022 · 1 comment · Fixed by #1733
Closed

CipherCtx::cipher_update is too pessimistic #1729

wiktor-k opened this issue Nov 9, 2022 · 1 comment · Fixed by #1733

Comments

@wiktor-k
Copy link
Sponsor Contributor

wiktor-k commented Nov 9, 2022

Hi,

I'm using the symmetric cipher encryption API and it seems the constraints in CipherCtx::cipher_update are too strict for block ciphers compared to what OpenSSL requires.

Currently the code checks if the output size has sufficient size and for streaming ciphers it's OK but for block ciphers it always assumes pessimistic case of partial block updates - that is feeding inputs that are smaller than the block size. Then, and only then the output needs to be big enough to contain the extra block.

I'm using block ciphers in a very strict case of always feeding data one entire block at a time and I'd like the output buffer to also be one block of length (this would make it consistent with other cryptographical backends that we use and also consistent with OpenSSL itself).

I see the following solutions:

  1. introduce an unsafe function that would not require the output buffer to be big enough for additional block. I've written a sample code and tested that against our data:
    pub unsafe fn unchecked_cipher_update(
        &mut self,
        input: &[u8],
        output: &mut [u8],
    ) -> Result<usize, ErrorStack> {
        let inlen = LenType::try_from(input.len()).unwrap();

        let block_size = self.block_size();
        if block_size > 1 {
            // block cipher
            assert_eq!(block_size, input.len());
            assert!(output.len() >= block_size);
        } else {
            // stream cipher
            assert_eq!(input.len(), output.len());
        }

        let mut outlen = 0;

        cvt(ffi::EVP_CipherUpdate(
            self.as_ptr(),
            output.as_mut_ptr(),
            &mut outlen,
            input.as_ptr(),
            inlen,
        ))?;

        Ok(outlen as usize)
    }

(Full code at wiktor-k@7d0fff8). Note that this function is unsafe only when mixed with partial block updates with CipherCtx::cipher_update. The disadvantage of this approach is exposing unsafe functions.

  1. Create other struct that has almost identical API but only allows stream ciphers or block ciphers where input.len() == block_size == output.len(). This has the advantage of offering safe API but the disadvantage of duplicating code (although for my use case only parts are needed).

  2. In CipherCtx keep track of how many bytes were fed into update and thus always require the output buffer to be of correct size. The advantage is that no public API would change and the cipher_update would just become more flexible. The disadvantage: additional complexity of tracking bytes modulo block size.

  3. Do not change anything and just make clients use raw ffi API. I'd like to avoid it but if there's no better solution I'd have to do that.

Maybe there are some other better ideas, if so I'm all ears!

Thank you for your time and sorry that this issue is so long 🙇


For reference I got the exact algorithm description from Matt from OpenSSL (source):

EVP_EncryptUpdate() can be called repeatedly, incrementally feeding in
the data to be encrypted. The ECB mode (when used with AES-128) will
encrypt input data 16 bytes at a time, and the output size will also be
16 bytes per input block. If the data that you feed in to
EVP_EncryptUpdate() is not a multiple of 16 bytes then the amount of
data that is over a multiple of 16 bytes will be cached until a
subsequent call where it does have 16 bytes.

Let's say you call EVP_EncryptUpdate() with 15 bytes of data. In that
case all 15 bytes will be cached and 0 bytes will be output.

If you then call it again with 17 bytes of data, then added to the 15
bytes already cached we have a total of 32 bytes. This is a multiple of
16, so 2 blocks (32 bytes) will be output, so:

(inl + cipher_block_size - 1) = (17 + 16 - 1) = 32

@sfackler
Copy link
Owner

sfackler commented Nov 9, 2022

I'd like to avoid having CipherCtx track any extra state internally. If there's a way to access the internal accounting of how much sub-block data is currently buffered then we could hopefully use that to have a more accurate output buffer check. Otherwise, adding an unchecked update method like you suggested would probably be the way to go.

wiktor-k added a commit to wiktor-k/rust-openssl that referenced this issue Nov 14, 2022
This change relaxes constraints on the output buffer size when it can be
safely determined how many bytes will be put in the output buffer.

For supported cryptographic backends (OpenSSL >= 1.1) the cipher's `num`
parameter will be consulted for the number of bytes in the block cache.
For unsupported backends the behavior will not change (the code will
assume full block in the cache).

For callers that do the check themselves and want to use other backends
(e.g. BoringSSL or LibreSSL) and unsafe `unchecked_cipher_update`
function is added.

Additionally a `CipherCtx::minimal_output_size` function is added for
letting the callers know how big should the output buffer be for the
next `cipher_update` call.

Fixes sfackler#1729.

See: https://mta.openssl.org/pipermail/openssl-users/2022-November/015623.html
wiktor-k added a commit to wiktor-k/rust-openssl that referenced this issue Nov 14, 2022
This change relaxes constraints on the output buffer size when it can be
safely determined how many bytes will be put in the output buffer.

For supported cryptographic backends (OpenSSL >= 1.1) the cipher's `num`
parameter will be consulted for the number of bytes in the block cache.
For unsupported backends the behavior will not change (the code will
assume full block in the cache).

For callers that do the check themselves and want to use other backends
(e.g. BoringSSL or LibreSSL) and unsafe `unchecked_cipher_update`
function is added.

Additionally a `CipherCtx::minimal_output_size` function is added for
letting the callers know how big should the output buffer be for the
next `cipher_update` call.

Fixes sfackler#1729.

See: https://mta.openssl.org/pipermail/openssl-users/2022-November/015623.html
wiktor-k added a commit to wiktor-k/rust-openssl that referenced this issue Nov 24, 2022
This change relaxes constraints on the output buffer size when it can be
safely determined how many bytes will be put in the output buffer.

For supported cryptographic backends (OpenSSL >= 1.1) the cipher's `num`
parameter will be consulted for the number of bytes in the block cache.
For unsupported backends the behavior will not change (the code will
assume full block in the cache).

For callers that do the check themselves and want to use other backends
(e.g. BoringSSL or LibreSSL) and unsafe `cipher_update_unchecked`
function is added.

Additionally a `CipherCtx::minimal_output_size` function is added for
letting the callers know how big should the output buffer be for the
next `cipher_update` call.

Fixes sfackler#1729.

See: https://mta.openssl.org/pipermail/openssl-users/2022-November/015623.html
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants