From 318c34c0920dfb11d2f7bc0c1a74e7ab9bd43403 Mon Sep 17 00:00:00 2001 From: Will Childs-Klein Date: Tue, 29 Oct 2024 16:09:19 -0400 Subject: [PATCH] Add PKCS7-internal BIO_f_cipher (#1836) This change introduces a new filter BIO, `BIO_f_cipher` for use in PR 1816. The cipher BIO sits in front of a backing BIO, encrypting incoming writes before writing to the backing BIO and decrypting data read from the backing BIO. This implementation is almost an exact copy of [OpenSSL's](https://github.com/openssl/openssl/blob/8e0d479b98357bb20ab1bd073cf75f7d42531553/crypto/evp/bio_enc.c#L59) with some functionality removed. We try to change as little of the underlying logic as possible, but rename variables and add comments for clarity. --- crypto/CMakeLists.txt | 2 + crypto/pkcs7/bio/bio_cipher_test.cc | 336 ++++++++++++++++++++++++++++ crypto/pkcs7/bio/cipher.c | 315 ++++++++++++++++++++++++++ crypto/pkcs7/internal.h | 11 + 4 files changed, 664 insertions(+) create mode 100644 crypto/pkcs7/bio/bio_cipher_test.cc create mode 100644 crypto/pkcs7/bio/cipher.c diff --git a/crypto/CMakeLists.txt b/crypto/CMakeLists.txt index b2314d263c..ac20508641 100644 --- a/crypto/CMakeLists.txt +++ b/crypto/CMakeLists.txt @@ -460,6 +460,7 @@ add_library( pem/pem_pkey.c pem/pem_x509.c pem/pem_xaux.c + pkcs7/bio/cipher.c pkcs7/pkcs7.c pkcs7/pkcs7_asn1.c pkcs7/pkcs7_x509.c @@ -814,6 +815,7 @@ if(BUILD_TESTING) obj/obj_test.cc ocsp/ocsp_test.cc pem/pem_test.cc + pkcs7/bio/bio_cipher_test.cc pkcs7/pkcs7_test.cc pkcs8/pkcs8_test.cc pkcs8/pkcs12_test.cc diff --git a/crypto/pkcs7/bio/bio_cipher_test.cc b/crypto/pkcs7/bio/bio_cipher_test.cc new file mode 100644 index 0000000000..d111d27476 --- /dev/null +++ b/crypto/pkcs7/bio/bio_cipher_test.cc @@ -0,0 +1,336 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR ISC + +#include + +#include +#include +#include + +#include "../../test/test_util.h" +#include "../internal.h" + +// NOTE: need to keep this in sync with sizeof(ctx->buf) cipher.c +#define ENC_BLOCK_SIZE 1024 * 4 + +#define BIO_get_cipher_status(bio) \ + BIO_ctrl(bio, BIO_C_GET_CIPHER_STATUS, 0, NULL) + +struct CipherParams { + const char name[40]; + const EVP_CIPHER *(*cipher)(void); +}; + +static const struct CipherParams Ciphers[] = { + {"AES_128_CBC", EVP_aes_128_cbc}, + {"AES_128_CTR", EVP_aes_128_ctr}, + {"AES_128_OFB", EVP_aes_128_ofb}, + {"AES_256_CBC", EVP_aes_256_cbc}, + {"AES_256_CTR", EVP_aes_256_ctr}, + {"AES_256_OFB", EVP_aes_256_ofb}, + {"ChaCha20Poly1305", EVP_chacha20_poly1305}, +}; + +class BIOCipherTest : public testing::TestWithParam {}; + +INSTANTIATE_TEST_SUITE_P(PKCS7Test, BIOCipherTest, testing::ValuesIn(Ciphers), + [](const testing::TestParamInfo ¶ms) + -> std::string { return params.param.name; }); + +TEST_P(BIOCipherTest, Basic) { + uint8_t key[EVP_MAX_KEY_LENGTH]; + uint8_t iv[EVP_MAX_IV_LENGTH]; + uint8_t pt[ENC_BLOCK_SIZE * 2]; + uint8_t pt_decrypted[sizeof(pt)]; + uint8_t ct[sizeof(pt) + EVP_MAX_BLOCK_LENGTH]; // pt + pad + bssl::UniquePtr bio_cipher; + bssl::UniquePtr bio_mem; + std::vector pt_vec, ct_vec, decrypted_pt_vec; + uint8_t buff[2 * sizeof(pt)]; + + const EVP_CIPHER *cipher = GetParam().cipher(); + ASSERT_TRUE(cipher); + + OPENSSL_cleanse(buff, sizeof(buff)); + OPENSSL_cleanse(ct, sizeof(ct)); + OPENSSL_cleanse(pt_decrypted, sizeof(pt_decrypted)); + OPENSSL_memset(pt, 'A', sizeof(pt)); + OPENSSL_memset(key, 'B', sizeof(key)); + OPENSSL_memset(iv, 'C', sizeof(iv)); + + // Unsupported or unimplemented CTRL flags and cipher(s) + bio_cipher.reset(BIO_new(BIO_f_cipher())); + ASSERT_TRUE(bio_cipher); + EXPECT_FALSE(BIO_ctrl(bio_cipher.get(), BIO_CTRL_DUP, 0, NULL)); + EXPECT_FALSE(BIO_ctrl(bio_cipher.get(), BIO_CTRL_GET_CALLBACK, 0, NULL)); + EXPECT_FALSE(BIO_ctrl(bio_cipher.get(), BIO_CTRL_SET_CALLBACK, 0, NULL)); + EXPECT_FALSE(BIO_ctrl(bio_cipher.get(), BIO_C_DO_STATE_MACHINE, 0, NULL)); + EXPECT_FALSE(BIO_ctrl(bio_cipher.get(), BIO_C_GET_CIPHER_CTX, 0, NULL)); + EXPECT_FALSE(BIO_ctrl(bio_cipher.get(), BIO_C_SSL_MODE, 0, NULL)); + EXPECT_FALSE(BIO_set_cipher(bio_cipher.get(), EVP_rc4(), key, iv, /*enc*/ 1)); + + // Round-trip using |BIO_write| for encryption with same BIOs, reset between + // encryption/decryption using |BIO_reset|. Fixed size IO. + bio_cipher.reset(BIO_new(BIO_f_cipher())); + ASSERT_TRUE(bio_cipher); + EXPECT_TRUE(BIO_set_cipher(bio_cipher.get(), cipher, key, iv, /*enc*/ 1)); + bio_mem.reset(BIO_new(BIO_s_mem())); + ASSERT_TRUE(bio_mem); + ASSERT_TRUE(BIO_push(bio_cipher.get(), bio_mem.get())); + // Copy |pt| contents to |ct| so we can detect that |ct| gets overwritten + OPENSSL_memcpy(ct, pt, sizeof(pt)); + OPENSSL_cleanse(pt_decrypted, sizeof(pt_decrypted)); + EXPECT_TRUE(BIO_eof(bio_cipher.get())); + EXPECT_EQ(0UL, BIO_wpending(bio_cipher.get())); + EXPECT_TRUE(BIO_write(bio_cipher.get(), pt, sizeof(pt))); + EXPECT_FALSE(BIO_eof(bio_cipher.get())); + EXPECT_EQ(0UL, BIO_wpending(bio_cipher.get())); + EXPECT_TRUE(BIO_flush(bio_cipher.get())); + EXPECT_EQ(0UL, BIO_wpending(bio_cipher.get())); + EXPECT_TRUE(BIO_get_cipher_status(bio_cipher.get())); + int ct_size = BIO_read(bio_mem.get(), ct, sizeof(ct)); + ASSERT_LE((size_t)ct_size, sizeof(ct)); + // first block should now differ + EXPECT_NE(Bytes(pt, EVP_MAX_BLOCK_LENGTH), Bytes(ct, EVP_MAX_BLOCK_LENGTH)); + // Reset both BIOs and decrypt + EXPECT_TRUE(BIO_reset(bio_cipher.get())); // also resets owned |bio_mem| + EXPECT_TRUE(BIO_write(bio_mem.get(), ct, ct_size)); + bio_mem.release(); // |bio_cipher| took ownership + EXPECT_TRUE(BIO_set_cipher(bio_cipher.get(), cipher, key, iv, /*enc*/ 0)); + EXPECT_TRUE(BIO_read(bio_cipher.get(), pt_decrypted, sizeof(pt_decrypted))); + EXPECT_TRUE(BIO_get_cipher_status(bio_cipher.get())); + EXPECT_EQ(Bytes(pt, sizeof(pt)), Bytes(pt_decrypted, sizeof(pt_decrypted))); + + // Test a number of different IO sizes around byte, cipher block, + // internal buffer size, and other boundaries. + int io_sizes[] = {1, + 3, + 7, + 8, + 9, + 64, + 923, + sizeof(pt), + 15, + 16, + 17, + 31, + 32, + 33, + 511, + 512, + 513, + 1023, + 1024, + 1025, + ENC_BLOCK_SIZE - 1, + ENC_BLOCK_SIZE, + ENC_BLOCK_SIZE + 1}; + + // Round-trip encryption/decryption with successive IOs of different sizes. + bio_cipher.reset(BIO_new(BIO_f_cipher())); + ASSERT_TRUE(bio_cipher); + EXPECT_TRUE(BIO_set_cipher(bio_cipher.get(), cipher, key, iv, /*enc*/ 1)); + bio_mem.reset(BIO_new(BIO_s_mem())); + ASSERT_TRUE(bio_mem); + ASSERT_TRUE(BIO_push(bio_cipher.get(), bio_mem.get())); + for (size_t wsize : io_sizes) { + pt_vec.insert(pt_vec.end(), pt, pt + wsize); + EXPECT_TRUE(BIO_write(bio_cipher.get(), pt, wsize)); + } + EXPECT_TRUE(BIO_flush(bio_cipher.get())); + EXPECT_TRUE(BIO_get_cipher_status(bio_cipher.get())); + while (!BIO_eof(bio_mem.get())) { + size_t bytes_read = BIO_read(bio_mem.get(), buff, sizeof(buff)); + ct_vec.insert(ct_vec.end(), buff, buff + bytes_read); + } + EXPECT_TRUE(BIO_reset(bio_cipher.get())); // also resets owned |bio_mem| + EXPECT_TRUE( + BIO_write(bio_mem.get(), ct_vec.data(), ct_vec.size())); // replace ct + bio_mem.release(); // |bio_cipher| took ownership + EXPECT_TRUE(BIO_set_cipher(bio_cipher.get(), cipher, key, iv, /*enc*/ 0)); + for (size_t rsize : io_sizes) { + EXPECT_TRUE(BIO_read(bio_cipher.get(), buff, rsize)); + decrypted_pt_vec.insert(decrypted_pt_vec.end(), buff, buff + rsize); + } + EXPECT_TRUE(BIO_get_cipher_status(bio_cipher.get())); + EXPECT_EQ(pt_vec.size(), decrypted_pt_vec.size()); + EXPECT_EQ(Bytes(pt_vec.data(), pt_vec.size()), + Bytes(decrypted_pt_vec.data(), decrypted_pt_vec.size())); + + // Induce IO failures in the underlying BIO between subsequent same-size + // operations. The flow of this test is to, for each IO size: + // + // 1. Write/encrypt a chunk of plaintext. + // 2. Disable writes in the underlying BIO and try to write the same plaintext + // chunk again. depending on how large the write size relative to cipher + // BIO's internal buffer size, the write may partially or fully succeed if + // it can be buffered. + // 3. Enable writes in the underlying BIO and complete 2.'s chunk by writing + // any remaining bytes in the chunk + // 4. Flush the cipher BIO to complete the encryption, reset the cipher BIO in + // decrypt mode with the underlying BIO containing the ciphertext. + // 5. Similar to 1., read/decrypt a chunk of ciphertext. + // 6. Similar to 2., disable reads in the underlying BIO. As with 2., this may + // partially or fully succeed depending on how large the read is relative + // to internal buffer sizes. + // 7. Enable reads in the underlying BIO and decrypt the rest of the + // ciphertext. + // 8. Compare original and decrypted plaintexts. + int rsize, wsize; + for (int io_size : io_sizes) { + pt_vec.clear(); + decrypted_pt_vec.clear(); + bio_cipher.reset(BIO_new(BIO_f_cipher())); + ASSERT_TRUE(bio_cipher); + EXPECT_TRUE(BIO_set_cipher(bio_cipher.get(), cipher, key, iv, /*enc*/ 1)); + bio_mem.reset(BIO_new(BIO_s_mem())); + ASSERT_TRUE(bio_mem); + ASSERT_TRUE(BIO_push(bio_cipher.get(), bio_mem.get())); + // Initial write should fully succeed + wsize = BIO_write(bio_cipher.get(), pt, io_size); + if (wsize > 0) { + pt_vec.insert(pt_vec.end(), pt, pt + wsize); + } + EXPECT_EQ(io_size, wsize); + // All data should have been written through to underlying BIO + EXPECT_EQ(0UL, BIO_wpending(bio_cipher.get())); + // Set underlying BIO to r/o to induce buffering in |bio_cipher| + auto disable_writes = [](BIO *bio, int oper, const char *argp, size_t len, + int argi, long argl, int bio_ret, + size_t *processed) -> long { + return (oper & BIO_CB_RETURN) || !(oper & BIO_CB_WRITE); + }; + BIO_set_callback_ex(bio_mem.get(), disable_writes); + BIO_set_retry_write(bio_mem.get()); + int full_buffer = ENC_BLOCK_SIZE; + // EVP block ciphers need up to EVP_MAX_BLOCK_LENGTH-1 bytes reserved + if (EVP_CIPHER_block_size(cipher) > 1) { + full_buffer -= EVP_CIPHER_block_size(cipher) - 1; + } + // Write to |bio_cipher| should still succeed in writing up to + // ENC_BLOCK_SIZE bytes by buffering them + wsize = BIO_write(bio_cipher.get(), pt, io_size); + if (wsize > 0) { + pt_vec.insert(pt_vec.end(), pt, pt + wsize); + } + // First write succeeds due to write buffering up to |ENC_BLOCK_SIZE| bytes + if (io_size >= full_buffer) { + EXPECT_EQ(full_buffer, wsize); + } else { + EXPECT_GT(full_buffer, wsize); + } + // If buffer is full, writes will fail + if (BIO_wpending(bio_cipher.get()) >= (size_t)full_buffer) { + EXPECT_FALSE(BIO_write(bio_cipher.get(), pt, sizeof(pt))); + } + // Writes still disabled, so flush fails and we have data pending + EXPECT_FALSE(BIO_flush(bio_cipher.get())); + EXPECT_GT(BIO_wpending(bio_cipher.get()), 0UL); + // Re-enable writes + BIO_set_callback_ex(bio_mem.get(), nullptr); + BIO_clear_retry_flags(bio_mem.get()); + if (wsize < io_size) { + const int remaining = io_size - wsize; + ASSERT_EQ(remaining, BIO_write(bio_cipher.get(), pt, remaining)); + pt_vec.insert(pt_vec.end(), pt, pt + remaining); + } + // Flush should empty the buffered encrypted data + EXPECT_TRUE(BIO_flush(bio_cipher.get())); + EXPECT_EQ(0UL, BIO_wpending(bio_cipher.get())); + EXPECT_TRUE(BIO_get_cipher_status(bio_cipher.get())); + EXPECT_TRUE(BIO_set_cipher(bio_cipher.get(), cipher, key, iv, /*enc*/ 0)); + // Reset BIOs, hydrate ciphertext for decryption + ct_vec.clear(); + while ((rsize = BIO_read(bio_mem.get(), buff, io_size)) > 0) { + ct_vec.insert(ct_vec.end(), buff, buff + rsize); + } + EXPECT_TRUE(BIO_reset(bio_cipher.get())); // also resets owned |bio_mem| + ASSERT_EQ((int)ct_vec.size(), BIO_write(bio_mem.get(), ct_vec.data(), + ct_vec.size())); // replace ct + EXPECT_LE(pt_vec.size(), BIO_pending(bio_cipher.get())); + // First read should fully succeed + rsize = BIO_read(bio_cipher.get(), buff, io_size); + ASSERT_EQ(io_size, rsize); + decrypted_pt_vec.insert(decrypted_pt_vec.end(), buff, buff + rsize); + // Disable reads from underlying BIO + auto disable_reads = [](BIO *bio, int oper, const char *argp, size_t len, + int argi, long argl, int bio_ret, + size_t *processed) -> long { + return (oper & BIO_CB_RETURN) || !(oper & BIO_CB_READ); + }; + BIO_set_callback_ex(bio_mem.get(), disable_reads); + // Set retry flags so |cipher_bio| doesn't give up when the read fails + BIO_set_retry_read(bio_mem.get()); + rsize = BIO_read(bio_cipher.get(), buff, io_size); + decrypted_pt_vec.insert(decrypted_pt_vec.end(), buff, buff + rsize); + EXPECT_EQ(0UL, BIO_pending(bio_cipher.get())); + // Re-enable reads from underlying BIO + BIO_set_callback_ex(bio_mem.get(), nullptr); + BIO_clear_retry_flags(bio_mem.get()); + while ((rsize = BIO_read(bio_cipher.get(), buff, io_size)) > 0) { + decrypted_pt_vec.insert(decrypted_pt_vec.end(), buff, buff + rsize); + } + EXPECT_TRUE(BIO_eof(bio_cipher.get())); + EXPECT_EQ(0UL, BIO_pending(bio_cipher.get())); + EXPECT_TRUE(BIO_get_cipher_status(bio_cipher.get())); + EXPECT_EQ(pt_vec.size(), decrypted_pt_vec.size()); + EXPECT_EQ(Bytes(pt_vec.data(), pt_vec.size()), + Bytes(decrypted_pt_vec.data(), decrypted_pt_vec.size())); + bio_mem.release(); // |bio_cipher| took ownership + } +} + +TEST_P(BIOCipherTest, Randomized) { + uint8_t key[EVP_MAX_KEY_LENGTH], iv[EVP_MAX_IV_LENGTH], buff[8 * 1024]; + bssl::UniquePtr bio_cipher, bio_mem; + std::vector pt, ct, decrypted; + + const EVP_CIPHER *cipher = GetParam().cipher(); + ASSERT_TRUE(cipher); + + OPENSSL_memset(key, 'X', sizeof(key)); + OPENSSL_memset(iv, 'Y', sizeof(iv)); + for (int i = 0; i < (int)sizeof(buff); i++) { + int n = i % 16; + char c = n < 10 ? '0' + n : 'A' + (n - 10); + buff[i] = c; + } + + // Round-trip using |BIO_write| for encryption with same BIOs, reset between + // encryption/decryption using |BIO_reset|. Fixed size IO. + bio_cipher.reset(BIO_new(BIO_f_cipher())); + BIO_set_cipher(bio_cipher.get(), cipher, key, iv, /*enc*/ 1); + bio_mem.reset(BIO_new(BIO_s_mem())); + BIO_push(bio_cipher.get(), bio_mem.get()); + int total_bytes = 0; + srand(42); + for (int i = 0; i < 1000; i++) { + int n = (rand() % (sizeof(buff) - 1)) + 1; + ASSERT_TRUE(BIO_write(bio_cipher.get(), buff, n)); + pt.insert(pt.end(), buff, buff + n); + total_bytes += n; + } + EXPECT_TRUE(BIO_flush(bio_cipher.get())); + EXPECT_TRUE(BIO_get_cipher_status(bio_cipher.get())); + int rsize; + while ((rsize = BIO_read(bio_mem.get(), buff, sizeof(buff))) > 0) { + ct.insert(ct.end(), buff, buff + rsize); + } + // only consider first |pt.size()| bytes of |ct|, exclude pad block + EXPECT_NE(Bytes(pt.data(), pt.size()), Bytes(ct.data(), pt.size())); + // Reset both BIOs and decrypt + EXPECT_TRUE(BIO_reset(bio_cipher.get())); // also resets owned |bio_mem| + EXPECT_TRUE(BIO_write(bio_mem.get(), ct.data(), ct.size())); + bio_mem.release(); // |bio_cipher| took ownership + EXPECT_TRUE(BIO_set_cipher(bio_cipher.get(), cipher, key, iv, /*enc*/ 0)); + EXPECT_FALSE(BIO_eof(bio_cipher.get())); + while ((rsize = BIO_read(bio_cipher.get(), buff, sizeof(buff))) > 0) { + decrypted.insert(decrypted.end(), buff, buff + rsize); + } + EXPECT_TRUE(BIO_eof(bio_cipher.get())); + EXPECT_TRUE(BIO_get_cipher_status(bio_cipher.get())); + EXPECT_EQ(Bytes(pt.data(), pt.size()), + Bytes(decrypted.data(), decrypted.size())); + EXPECT_EQ(total_bytes, (int)decrypted.size()); +} diff --git a/crypto/pkcs7/bio/cipher.c b/crypto/pkcs7/bio/cipher.c new file mode 100644 index 0000000000..f80772200a --- /dev/null +++ b/crypto/pkcs7/bio/cipher.c @@ -0,0 +1,315 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR ISC + +#include +#include +#include +#include +#include +#include +#include "../../fipsmodule/cipher/internal.h" +#include "../internal.h" + +typedef struct enc_struct { + uint8_t done; // indicates "EOF" for read, "flushed" for write + uint8_t ok; // cipher status, either 0 (error) or 1 (ok) + int buf_off; // start idx of buffered data + int buf_len; // length of buffered data + EVP_CIPHER_CTX *cipher; + uint8_t buf[1024 * 4]; // plaintext for read, ciphertext for writes +} BIO_ENC_CTX; + +static int enc_new(BIO *b) { + BIO_ENC_CTX *ctx; + GUARD_PTR(b); + + if ((ctx = OPENSSL_zalloc(sizeof(*ctx))) == NULL) { + return 0; + } + + ctx->cipher = EVP_CIPHER_CTX_new(); + if (ctx->cipher == NULL) { + OPENSSL_free(ctx); + return 0; + } + ctx->done = 0; + ctx->ok = 1; + ctx->buf_off = 0; + ctx->buf_len = 0; + BIO_set_data(b, ctx); + BIO_set_init(b, 1); + + return 1; +} + +static int enc_free(BIO *b) { + GUARD_PTR(b); + + BIO_ENC_CTX *ctx = BIO_get_data(b); + if (ctx == NULL) { + return 0; + } + + EVP_CIPHER_CTX_free(ctx->cipher); + OPENSSL_free(ctx); + BIO_set_data(b, NULL); + BIO_set_init(b, 0); + + return 1; +} + +static int enc_read(BIO *b, char *out, int outl) { + GUARD_PTR(b); + GUARD_PTR(out); + BIO_ENC_CTX *ctx = BIO_get_data(b); + if (ctx == NULL || ctx->cipher == NULL || !ctx->ok || outl <= 0) { + return 0; + } + BIO *next = BIO_next(b); + if (next == NULL) { + return 0; + } + + int bytes_output = 0; + int remaining = outl; + uint8_t read_buf[sizeof(ctx->buf)]; + const int cipher_block_size = EVP_CIPHER_CTX_block_size(ctx->cipher); + while ((!ctx->done || ctx->buf_len > 0) && remaining > 0) { + assert(bytes_output + remaining == outl); + if (ctx->buf_len > 0) { + uint8_t *out_pos = ((uint8_t *)out) + bytes_output; + int to_copy = remaining > ctx->buf_len ? ctx->buf_len : remaining; + OPENSSL_memcpy(out_pos, &ctx->buf[ctx->buf_off], to_copy); + // Update buffer info and counters with number of bytes processed from our + // buffer. + ctx->buf_len -= to_copy; + ctx->buf_off += to_copy; + bytes_output += to_copy; + remaining -= to_copy; + continue; + } + ctx->buf_len = 0; + ctx->buf_off = 0; + // |EVP_DecryptUpdate| may write up to cipher_block_size-1 more bytes than + // requested, so only read bytes we're sure we can decrypt in place. + int to_read = (int)sizeof(ctx->buf) - cipher_block_size + 1; + int bytes_read = BIO_read(next, read_buf, to_read); + if (bytes_read > 0) { + // Decrypt ciphertext in place, update |ctx->buf_len| with num bytes + // decrypted. + ctx->ok = EVP_DecryptUpdate(ctx->cipher, ctx->buf, &ctx->buf_len, + read_buf, bytes_read); + } else if (BIO_eof(next)) { + // EVP_DecryptFinal_ex may write up to one block to our buffer. If that + // happens, continue the loop to process the decrypted block as normal. + ctx->ok = EVP_DecryptFinal_ex(ctx->cipher, ctx->buf, &ctx->buf_len); + ctx->done = 1; // If we can't read any more bytes, set done. + } else { + // |BIO_read| returned <= 0, but no EOF. Copy retry and return. + if (bytes_read < 0 && !BIO_should_retry(next)) { + ctx->done = 1; + ctx->ok = 0; + } + BIO_copy_next_retry(b); + break; + } + if (!ctx->ok) { + ctx->done = 1; // Set EOF on cipher error. + } + } + return bytes_output; +} + +static int enc_flush(BIO *b, BIO *next, BIO_ENC_CTX *ctx) { + GUARD_PTR(b); + GUARD_PTR(next); + GUARD_PTR(ctx); + while (ctx->ok > 0 && (ctx->buf_len > 0 || !ctx->done)) { + int bytes_written = BIO_write(next, &ctx->buf[ctx->buf_off], ctx->buf_len); + if (ctx->buf_len > 0 && bytes_written <= 0) { + if (bytes_written < 0 && !BIO_should_retry(next)) { + ctx->done = 1; + ctx->ok = 0; + } + BIO_copy_next_retry(b); + return 0; + } + ctx->buf_off += bytes_written; + ctx->buf_len -= bytes_written; + if (ctx->buf_len == 0 && !ctx->done) { + ctx->done = 1; + ctx->buf_off = 0; + ctx->ok = EVP_EncryptFinal_ex(ctx->cipher, ctx->buf, &ctx->buf_len); + } + } + return ctx->ok; +} + +static int enc_write(BIO *b, const char *in, int inl) { + GUARD_PTR(b); + GUARD_PTR(in); + BIO_ENC_CTX *ctx = BIO_get_data(b); + if (ctx == NULL || ctx->cipher == NULL || ctx->done || !ctx->ok || inl <= 0) { + return 0; + } + BIO *next = BIO_next(b); + if (next == NULL) { + return 0; + } + + int bytes_consumed = 0; + int remaining = inl; + const int max_crypt_size = + (int)sizeof(ctx->buf) - EVP_CIPHER_CTX_block_size(ctx->cipher) + 1; + while ((!ctx->done || ctx->buf_len > 0) && remaining > 0) { + assert(bytes_consumed + remaining == inl); + if (ctx->buf_len == 0) { + ctx->buf_off = 0; + int to_encrypt = remaining < max_crypt_size ? remaining : max_crypt_size; + uint8_t *in_pos = ((uint8_t *)in) + bytes_consumed; + ctx->ok = EVP_EncryptUpdate(ctx->cipher, ctx->buf, &ctx->buf_len, in_pos, + to_encrypt); + if (!ctx->ok) { + break; + }; + bytes_consumed += to_encrypt; + remaining -= to_encrypt; + } + int bytes_written = BIO_write(next, &ctx->buf[ctx->buf_off], ctx->buf_len); + if (bytes_written <= 0) { + if (bytes_written < 0 && !BIO_should_retry(next)) { + ctx->done = 1; + ctx->ok = 0; + } + BIO_copy_next_retry(b); + break; + } + ctx->buf_off += bytes_written; + ctx->buf_len -= bytes_written; + } + return bytes_consumed; +} + +static long enc_ctrl(BIO *b, int cmd, long num, void *ptr) { + GUARD_PTR(b); + long ret = 1; + + BIO_ENC_CTX *ctx = BIO_get_data(b); + BIO *next = BIO_next(b); + if (ctx == NULL) { + return 0; + } + + switch (cmd) { + case BIO_CTRL_RESET: + ctx->done = 0; + ctx->ok = 1; + ctx->buf_off = 0; + ctx->buf_len = 0; + OPENSSL_cleanse(ctx->buf, sizeof(ctx->buf)); + if (!EVP_CipherInit_ex(ctx->cipher, NULL, NULL, NULL, NULL, + EVP_CIPHER_CTX_encrypting(ctx->cipher))) { + return 0; + } + ret = BIO_ctrl(next, cmd, num, ptr); + break; + case BIO_CTRL_EOF: + if (ctx->done) { + ret = 1; + } else { + ret = BIO_ctrl(next, cmd, num, ptr); + } + break; + case BIO_CTRL_WPENDING: + case BIO_CTRL_PENDING: + // Return number of bytes left to process if we have anything buffered, + // else consult underlying BIO. + ret = ctx->buf_len; + if (ret <= 0) { + ret = BIO_ctrl(next, cmd, num, ptr); + } + break; + case BIO_CTRL_FLUSH: + ret = enc_flush(b, next, ctx); + if (ret <= 0) { + break; + } + // Flush the underlying BIO + ret = BIO_ctrl(next, cmd, num, ptr); + BIO_copy_next_retry(b); + break; + case BIO_C_GET_CIPHER_STATUS: + ret = (long)ctx->ok; + break; + // OpenSSL implements these, but because we don't need them and cipher BIO + // is internal, we can fail loudly if they're called. If this case is hit, + // it likely means you're making a change that will require implementing + // these. + case BIO_CTRL_DUP: + case BIO_CTRL_GET_CALLBACK: + case BIO_CTRL_SET_CALLBACK: + case BIO_C_DO_STATE_MACHINE: + case BIO_C_GET_CIPHER_CTX: + OPENSSL_PUT_ERROR(PKCS7, ERR_R_BIO_LIB); + return 0; + default: + ret = BIO_ctrl(next, cmd, num, ptr); + break; + } + return ret; +} + +int BIO_set_cipher(BIO *b, const EVP_CIPHER *c, const unsigned char *key, + const unsigned char *iv, int enc) { + GUARD_PTR(b); + GUARD_PTR(c); + + BIO_ENC_CTX *ctx = BIO_get_data(b); + if (ctx == NULL) { + return 0; + } + + // We only support a modern subset of available EVP_CIPHERs. Other ciphers + // (e.g. DES) and cipher modes (e.g. CBC, CCM) had issues with block alignment + // and padding during testing, so they're forbidden for now. + const EVP_CIPHER *kSupportedCiphers[] = { + EVP_aes_128_cbc(), EVP_aes_128_ctr(), EVP_aes_128_ofb(), + EVP_aes_256_cbc(), EVP_aes_256_ctr(), EVP_aes_256_ofb(), + EVP_chacha20_poly1305(), + }; + const size_t kSupportedCiphersCount = + sizeof(kSupportedCiphers) / sizeof(EVP_CIPHER *); + int supported = 0; + for (size_t i = 0; i < kSupportedCiphersCount; i++) { + if (c == kSupportedCiphers[i]) { + supported = 1; + break; + } + } + if (!supported) { + OPENSSL_PUT_ERROR(PKCS7, ERR_R_BIO_LIB); + return 0; + } + + if (!EVP_CipherInit_ex(ctx->cipher, c, NULL, key, iv, enc)) { + return 0; + } + BIO_set_init(b, 1); + + return 1; +} + +static const BIO_METHOD methods_enc = { + BIO_TYPE_CIPHER, // type + "cipher", // name + enc_write, // bwrite + enc_read, // bread + NULL, // bputs + NULL, // bgets + enc_ctrl, // ctrl + enc_new, // create + enc_free, // destroy + NULL, // callback_ctrl +}; + +const BIO_METHOD *BIO_f_cipher(void) { return &methods_enc; } diff --git a/crypto/pkcs7/internal.h b/crypto/pkcs7/internal.h index 4cdda60d00..4a84f2ce17 100644 --- a/crypto/pkcs7/internal.h +++ b/crypto/pkcs7/internal.h @@ -200,6 +200,17 @@ int pkcs7_add_signed_data(CBB *out, const void *arg); +// BIO_f_cipher is used internally by the pkcs7 module. It is not recommended +// for external use. +OPENSSL_EXPORT const BIO_METHOD *BIO_f_cipher(void); + +// BIO_set_cipher is used internally for testing. It is not recommended for +// external use. +OPENSSL_EXPORT int BIO_set_cipher(BIO *b, const EVP_CIPHER *cipher, + const unsigned char *key, + const unsigned char *iv, int enc); + + #if defined(__cplusplus) } // extern C #endif