From bd158f6e6122359b5679ccd2d44b16173aed6682 Mon Sep 17 00:00:00 2001 From: WillChilds-Klein Date: Fri, 6 Sep 2024 22:39:51 +0000 Subject: [PATCH] Add PKCS7-internal cipher BIO --- crypto/CMakeLists.txt | 2 + crypto/pkcs7/internal.h | 9 + crypto/pkcs7/pkcs7_internal_bio_cipher.c | 398 +++++++++++++++++++++++ crypto/pkcs7/pkcs7_internal_bio_test.cc | 60 ++++ 4 files changed, 469 insertions(+) create mode 100644 crypto/pkcs7/pkcs7_internal_bio_cipher.c create mode 100644 crypto/pkcs7/pkcs7_internal_bio_test.cc diff --git a/crypto/CMakeLists.txt b/crypto/CMakeLists.txt index 606276ade01..6025f279e36 100644 --- a/crypto/CMakeLists.txt +++ b/crypto/CMakeLists.txt @@ -467,6 +467,7 @@ add_library( pem/pem_xaux.c pkcs7/pkcs7.c pkcs7/pkcs7_asn1.c + pkcs7/pkcs7_internal_bio_cipher.c pkcs7/pkcs7_x509.c pkcs8/pkcs8.c pkcs8/pkcs8_x509.c @@ -817,6 +818,7 @@ if(BUILD_TESTING) obj/obj_test.cc ocsp/ocsp_test.cc pem/pem_test.cc + pkcs7/pkcs7_internal_bio_test.cc pkcs7/pkcs7_test.cc pkcs8/pkcs8_test.cc pkcs8/pkcs12_test.cc diff --git a/crypto/pkcs7/internal.h b/crypto/pkcs7/internal.h index 4cdda60d001..25d31a0d415 100644 --- a/crypto/pkcs7/internal.h +++ b/crypto/pkcs7/internal.h @@ -199,6 +199,15 @@ int pkcs7_add_signed_data(CBB *out, int (*signer_infos_cb)(CBB *out, const void *arg), const void *arg); +// TODO [childw] +const BIO_METHOD *BIO_f_cipher(void); + +#define BIO_get_cipher_ctx(bio, contents) \ + BIO_ctrl(bio, BIO_C_GET_CIPHER_CTX, 0, (char *)(contents)) + +#define BIO_get_cipher_status(bio) \ + BIO_ctrl(bio, BIO_C_GET_CIPHER_STATUS, 0, NULL) + #if defined(__cplusplus) } // extern C diff --git a/crypto/pkcs7/pkcs7_internal_bio_cipher.c b/crypto/pkcs7/pkcs7_internal_bio_cipher.c new file mode 100644 index 00000000000..87048064aa3 --- /dev/null +++ b/crypto/pkcs7/pkcs7_internal_bio_cipher.c @@ -0,0 +1,398 @@ +// 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 "../crypto/bio/internal.h" +#include "../internal.h" +#include "./internal.h" + +static int enc_write(BIO *h, const char *buf, int num); +static int enc_read(BIO *h, char *buf, int size); +static long enc_ctrl(BIO *h, int cmd, long arg1, void *arg2); +static int enc_new(BIO *h); +static int enc_free(BIO *data); +static long enc_callback_ctrl(BIO *h, int cmd, bio_info_cb fps); +#define ENC_BLOCK_SIZE (1024 * 4) +#define ENC_MIN_CHUNK (256) +#define BUF_OFFSET (ENC_MIN_CHUNK + EVP_MAX_BLOCK_LENGTH) + +typedef struct enc_struct { + int buf_len; + int buf_off; + int cont; // <= 0 when finished + int finished; + int ok; // bad decrypt + EVP_CIPHER_CTX *cipher; + unsigned char *read_start, *read_end; + // buf is larger than ENC_BLOCK_SIZE because EVP_DecryptUpdate can return + // up to a block more data than is presented to it + unsigned char buf[BUF_OFFSET + ENC_BLOCK_SIZE]; +} BIO_ENC_CTX; + +static const BIO_METHOD methods_enc = { + BIO_TYPE_CIPHER, + "cipher", + enc_write, + enc_read, + NULL, // enc_puts + NULL, // enc_gets + enc_ctrl, + enc_new, + enc_free, + enc_callback_ctrl, +}; + +const BIO_METHOD *BIO_f_cipher(void) { return &methods_enc; } + +static int enc_new(BIO *bi) { + BIO_ENC_CTX *ctx; + + 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->cont = 1; + ctx->ok = 1; + ctx->read_end = ctx->read_start = &(ctx->buf[BUF_OFFSET]); + BIO_set_data(bi, ctx); + BIO_set_init(bi, 1); + + return 1; +} + +static int enc_free(BIO *a) { + BIO_ENC_CTX *b; + + if (a == NULL) { + return 0; + } + + b = BIO_get_data(a); + if (b == NULL) { + return 0; + } + + EVP_CIPHER_CTX_free(b->cipher); + OPENSSL_clear_free(b, sizeof(BIO_ENC_CTX)); + BIO_set_data(a, NULL); + BIO_set_init(a, 0); + + return 1; +} + +static int enc_read(BIO *b, char *out, int outl) { + int ret = 0, i, blocksize; + BIO_ENC_CTX *ctx; + BIO *next; + + if (out == NULL) { + return 0; + } + ctx = BIO_get_data(b); + + next = BIO_next(b); + if ((ctx == NULL) || (next == NULL)) { + return 0; + } + + // First check if there are bytes decoded/encoded + if (ctx->buf_len > 0) { + i = ctx->buf_len - ctx->buf_off; + if (i > outl) { + i = outl; + } + OPENSSL_memcpy(out, &(ctx->buf[ctx->buf_off]), i); + ret = i; + out += i; + outl -= i; + ctx->buf_off += i; + if (ctx->buf_len == ctx->buf_off) { + ctx->buf_len = 0; + ctx->buf_off = 0; + } + } + + blocksize = EVP_CIPHER_CTX_block_size(ctx->cipher); + + if (blocksize == 0) { + return 0; + } + + if (blocksize == 1) { + blocksize = 0; + } + + // At this point, we have room of outl bytes and an empty buffer, so we + // should read in some more. + while (outl > 0) { + if (ctx->cont <= 0) { + break; + } + + if (ctx->read_start == ctx->read_end) { // time to read more data + ctx->read_end = ctx->read_start = &(ctx->buf[BUF_OFFSET]); + i = BIO_read(next, ctx->read_start, ENC_BLOCK_SIZE); + if (i > 0) + ctx->read_end += i; + } else { + i = ctx->read_end - ctx->read_start; + } + + if (i <= 0) { + // Should be continue next time we are called? + if (!BIO_should_retry(next)) { + ctx->cont = i; + i = EVP_CipherFinal_ex(ctx->cipher, ctx->buf, &(ctx->buf_len)); + ctx->ok = i; + ctx->buf_off = 0; + } else { + ret = (ret == 0) ? i : ret; + break; + } + } else { + if (outl > ENC_MIN_CHUNK) { + // Depending on flags block cipher decrypt can write + // one extra block and then back off, i.e. output buffer + // has to accommodate extra block... + int j = outl - blocksize, buf_len; + + if (!EVP_CipherUpdate(ctx->cipher, (unsigned char *)out, &buf_len, + ctx->read_start, i > j ? j : i)) { + BIO_clear_retry_flags(b); + return 0; + } + ret += buf_len; + out += buf_len; + outl -= buf_len; + + if ((i -= j) <= 0) { + ctx->read_start = ctx->read_end; + continue; + } + ctx->read_start += j; + } + if (i > ENC_MIN_CHUNK) { + i = ENC_MIN_CHUNK; + } + if (!EVP_CipherUpdate(ctx->cipher, ctx->buf, &ctx->buf_len, + ctx->read_start, i)) { + BIO_clear_retry_flags(b); + ctx->ok = 0; + return 0; + } + ctx->read_start += i; + ctx->cont = 1; + // Note: it is possible for EVP_CipherUpdate to decrypt zero + // bytes because this is or looks like the final block: if this + // happens we should retry and either read more data or decrypt + // the final block + if (ctx->buf_len == 0) { + continue; + } + } + + if (ctx->buf_len <= outl) { + i = ctx->buf_len; + } else { + i = outl; + } + if (i <= 0) { + break; + } + OPENSSL_memcpy(out, ctx->buf, i); + ret += i; + ctx->buf_off = i; + outl -= i; + out += i; + } + + BIO_clear_retry_flags(b); + BIO_copy_next_retry(b); + return ((ret == 0) ? ctx->cont : ret); +} + +static int enc_write(BIO *b, const char *in, int inl) { + int ret = 0, n, i; + BIO_ENC_CTX *ctx; + BIO *next; + + ctx = BIO_get_data(b); + next = BIO_next(b); + if ((ctx == NULL) || (next == NULL)) { + return 0; + } + + ret = inl; + + BIO_clear_retry_flags(b); + n = ctx->buf_len - ctx->buf_off; + while (n > 0) { + i = BIO_write(next, &(ctx->buf[ctx->buf_off]), n); + if (i <= 0) { + BIO_copy_next_retry(b); + return i; + } + ctx->buf_off += i; + n -= i; + } + // at this point all pending data has been written + if ((in == NULL) || (inl <= 0)) { + return 0; + } + + ctx->buf_off = 0; + while (inl > 0) { + n = (inl > ENC_BLOCK_SIZE) ? ENC_BLOCK_SIZE : inl; + if (!EVP_CipherUpdate(ctx->cipher, ctx->buf, &ctx->buf_len, + (const unsigned char *)in, n)) { + BIO_clear_retry_flags(b); + ctx->ok = 0; + return 0; + } + inl -= n; + in += n; + + ctx->buf_off = 0; + n = ctx->buf_len; + while (n > 0) { + i = BIO_write(next, &(ctx->buf[ctx->buf_off]), n); + if (i <= 0) { + BIO_copy_next_retry(b); + return (ret == inl) ? i : ret - inl; + } + n -= i; + ctx->buf_off += i; + } + ctx->buf_len = 0; + ctx->buf_off = 0; + } + BIO_copy_next_retry(b); + return ret; +} + +static long enc_ctrl(BIO *b, int cmd, long num, void *ptr) { + BIO *dbio; + BIO_ENC_CTX *ctx, *dctx; + long ret = 1; + int i; + EVP_CIPHER_CTX **c_ctx; + BIO *next; + int pend; + + ctx = BIO_get_data(b); + next = BIO_next(b); + if (ctx == NULL) { + return 0; + } + + switch (cmd) { + case BIO_CTRL_RESET: + ctx->ok = 1; + ctx->finished = 0; + 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: // More to read + if (ctx->cont <= 0) { + ret = 1; + } else { + ret = BIO_ctrl(next, cmd, num, ptr); + } + break; + case BIO_CTRL_WPENDING: + ret = ctx->buf_len - ctx->buf_off; + if (ret <= 0) { + ret = BIO_ctrl(next, cmd, num, ptr); + } + break; + case BIO_CTRL_PENDING: // More to read in buffer + ret = ctx->buf_len - ctx->buf_off; + if (ret <= 0) { + ret = BIO_ctrl(next, cmd, num, ptr); + } + break; + case BIO_CTRL_FLUSH: + // do a final write + again: + while (ctx->buf_len != ctx->buf_off) { + pend = ctx->buf_len - ctx->buf_off; + i = enc_write(b, NULL, 0); + // i should never be > 0 here because we didn't ask to write any + // new data. We stop if we get an error or we failed to make any + // progress writing pending data. + if (i < 0 || (ctx->buf_len - ctx->buf_off) == pend) { + return i; + } + } + + if (!ctx->finished) { + ctx->finished = 1; + ctx->buf_off = 0; + ret = EVP_CipherFinal_ex(ctx->cipher, (unsigned char *)ctx->buf, + &(ctx->buf_len)); + ctx->ok = (int)ret; + if (ret <= 0) { + break; + } + + // push out the bytes + goto again; + } + + // Finally 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; + case BIO_C_DO_STATE_MACHINE: + BIO_clear_retry_flags(b); + ret = BIO_ctrl(next, cmd, num, ptr); + BIO_copy_next_retry(b); + break; + case BIO_C_GET_CIPHER_CTX: + c_ctx = (EVP_CIPHER_CTX **)ptr; + *c_ctx = ctx->cipher; + BIO_set_init(b, 1); + break; + case BIO_CTRL_DUP: + dbio = (BIO *)ptr; + dctx = BIO_get_data(dbio); + dctx->cipher = EVP_CIPHER_CTX_new(); + if (dctx->cipher == NULL) { + return 0; + } + ret = EVP_CIPHER_CTX_copy(dctx->cipher, ctx->cipher); + if (ret) { + BIO_set_init(dbio, 1); + } + break; + default: + ret = BIO_ctrl(next, cmd, num, ptr); + break; + } + return ret; +} + +static long enc_callback_ctrl(BIO *b, int cmd, bio_info_cb fp) { + BIO *next = BIO_next(b); + + if (next == NULL) { + return 0; + } + + return BIO_callback_ctrl(next, cmd, fp); +} diff --git a/crypto/pkcs7/pkcs7_internal_bio_test.cc b/crypto/pkcs7/pkcs7_internal_bio_test.cc new file mode 100644 index 00000000000..954f3749b62 --- /dev/null +++ b/crypto/pkcs7/pkcs7_internal_bio_test.cc @@ -0,0 +1,60 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../bytestring/internal.h" +#include "../internal.h" +#include "../test/test_util.h" +#include "./internal.h" + + +TEST(PKCS7Test, CipherBIO) { + uint8_t key[EVP_MAX_KEY_LENGTH]; + uint8_t iv[EVP_MAX_IV_LENGTH]; + uint8_t pt[1024]; + uint8_t pt_decrypted[sizeof(pt)]; + uint8_t ct[sizeof(pt)]; + EVP_CIPHER_CTX *ctx; + bssl::UniquePtr bio_cipher; + bssl::UniquePtr bio_mem; + + ASSERT_TRUE(RAND_bytes(pt, sizeof(pt))); + ASSERT_TRUE(RAND_bytes(key, sizeof(key))); + ASSERT_TRUE(RAND_bytes(iv, sizeof(iv))); + + bio_cipher.reset(BIO_new(BIO_f_cipher())); + ASSERT_TRUE(bio_cipher); + EXPECT_TRUE(BIO_get_cipher_ctx(bio_cipher.get(), &ctx)); + ASSERT_TRUE( + EVP_CipherInit_ex(ctx, EVP_aes_128_gcm(), NULL, key, iv, /*enc*/ 1)); + bio_mem.reset(BIO_new_mem_buf(pt, sizeof(pt))); + ASSERT_TRUE(bio_mem); + ASSERT_TRUE(BIO_up_ref(bio_mem.get())); // |bio_cipher| will take ownership + ASSERT_TRUE(BIO_set_mem_eof_return(bio_mem.get(), 0)); + ASSERT_TRUE(BIO_push(bio_cipher.get(), bio_mem.get())); + EXPECT_TRUE(BIO_read(bio_cipher.get(), ct, sizeof(ct))); + EXPECT_TRUE(BIO_flush(bio_cipher.get())); + EXPECT_TRUE(BIO_get_cipher_status(bio_cipher.get())); + // only consider first |sizeof(pt)| bytes of |ct|, exclude tag + EXPECT_NE(Bytes(pt, sizeof(pt)), Bytes(ct, sizeof(pt))); + + bio_cipher.reset(BIO_new(BIO_f_cipher())); + ASSERT_TRUE(bio_cipher); + EXPECT_TRUE(BIO_get_cipher_ctx(bio_cipher.get(), &ctx)); + ASSERT_TRUE( + EVP_CipherInit_ex(ctx, EVP_aes_128_gcm(), NULL, key, iv, /*enc*/ 0)); + bio_mem.reset(BIO_new_mem_buf((const uint8_t *)ct, sizeof(ct))); + ASSERT_TRUE(bio_mem); + ASSERT_TRUE(BIO_up_ref(bio_mem.get())); // |bio_cipher| will take ownership + ASSERT_TRUE(BIO_push(bio_cipher.get(), bio_mem.get())); + 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))); +}