Skip to content

Commit

Permalink
Add PKCS7-internal BIO_f_cipher (#1836)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
WillChilds-Klein authored Oct 29, 2024
1 parent 8d9809e commit 318c34c
Show file tree
Hide file tree
Showing 4 changed files with 664 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crypto/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
336 changes: 336 additions & 0 deletions crypto/pkcs7/bio/bio_cipher_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 OR ISC

#include <gtest/gtest.h>

#include <openssl/crypto.h>
#include <openssl/mem.h>
#include <openssl/x509.h>

#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<CipherParams> {};

INSTANTIATE_TEST_SUITE_P(PKCS7Test, BIOCipherTest, testing::ValuesIn(Ciphers),
[](const testing::TestParamInfo<CipherParams> &params)
-> 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> bio_cipher;
bssl::UniquePtr<BIO> bio_mem;
std::vector<uint8_t> 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> bio_cipher, bio_mem;
std::vector<uint8_t> 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());
}
Loading

0 comments on commit 318c34c

Please sign in to comment.