Skip to content

Commit eacb5fa

Browse files
author
mdbmes
authored
MONGOCRYPT-792 Avoid libcrypto lock contention and fetch overhead (#995)
1 parent 0e67249 commit eacb5fa

File tree

1 file changed

+233
-65
lines changed

1 file changed

+233
-65
lines changed

src/crypto/libcrypto.c

Lines changed: 233 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -33,47 +33,41 @@
3333
#include <openssl/hmac.h>
3434
#include <openssl/rand.h>
3535

36-
#if OPENSSL_VERSION_NUMBER < 0x10100000L || (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x20700000L)
37-
38-
static HMAC_CTX *HMAC_CTX_new(void) {
39-
return bson_malloc0(sizeof(HMAC_CTX));
40-
}
41-
42-
static void HMAC_CTX_free(HMAC_CTX *ctx) {
43-
HMAC_CTX_cleanup(ctx);
44-
bson_free(ctx);
45-
}
36+
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
37+
#include <openssl/core_names.h>
38+
#include <openssl/params.h>
4639
#endif
4740

4841
bool _native_crypto_initialized = false;
4942

50-
void _native_crypto_init(void) {
51-
_native_crypto_initialized = true;
52-
}
53-
54-
/* _encrypt_with_cipher encrypts @in with the OpenSSL cipher specified by
55-
* @cipher.
43+
/* _encrypt_with_cipher encrypts @in with the specified OpenSSL cipher.
44+
* @cipher is a usable EVP_CIPHER, or NULL if early initialization failed.
45+
* @cipher_description is a human-readable description used when reporting deferred errors from initialization, required
46+
* if @cipher might be NULL.
5647
* @key is the input key. @iv is the input IV.
5748
* @out is the output ciphertext. @out must be allocated by the caller with
5849
* enough room for the ciphertext.
5950
* @bytes_written is the number of bytes that were written to @out.
6051
* Returns false and sets @status on error. @status is required. */
61-
static bool _encrypt_with_cipher(const EVP_CIPHER *cipher, aes_256_args_t args) {
62-
EVP_CIPHER_CTX *ctx;
63-
bool ret = false;
64-
int intermediate_bytes_written = 0;
65-
mongocrypt_status_t *status = args.status;
66-
67-
ctx = EVP_CIPHER_CTX_new();
68-
52+
static bool _encrypt_with_cipher(const EVP_CIPHER *cipher, const char *cipher_description, aes_256_args_t args) {
6953
BSON_ASSERT(args.key);
7054
BSON_ASSERT(args.in);
7155
BSON_ASSERT(args.out);
72-
BSON_ASSERT(ctx);
73-
BSON_ASSERT(cipher);
56+
BSON_ASSERT(args.in->len <= INT_MAX);
57+
58+
mongocrypt_status_t *status = args.status;
59+
if (!cipher) {
60+
BSON_ASSERT(cipher_description);
61+
CLIENT_ERR("failed to initialize cipher %s", cipher_description);
62+
return false;
63+
}
64+
7465
BSON_ASSERT(NULL == args.iv || (uint32_t)EVP_CIPHER_iv_length(cipher) == args.iv->len);
7566
BSON_ASSERT((uint32_t)EVP_CIPHER_key_length(cipher) == args.key->len);
76-
BSON_ASSERT(args.in->len <= INT_MAX);
67+
68+
bool ret = false;
69+
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
70+
BSON_ASSERT(ctx);
7771

7872
if (!EVP_EncryptInit_ex(ctx, cipher, NULL /* engine */, args.key->data, NULL == args.iv ? NULL : args.iv->data)) {
7973
CLIENT_ERR("error in EVP_EncryptInit_ex: %s", ERR_error_string(ERR_get_error(), NULL));
@@ -84,6 +78,8 @@ static bool _encrypt_with_cipher(const EVP_CIPHER *cipher, aes_256_args_t args)
8478
EVP_CIPHER_CTX_set_padding(ctx, 0);
8579

8680
*args.bytes_written = 0;
81+
82+
int intermediate_bytes_written = 0;
8783
if (!EVP_EncryptUpdate(ctx, args.out->data, &intermediate_bytes_written, args.in->data, (int)args.in->len)) {
8884
CLIENT_ERR("error in EVP_EncryptUpdate: %s", ERR_error_string(ERR_get_error(), NULL));
8985
goto done;
@@ -107,30 +103,35 @@ static bool _encrypt_with_cipher(const EVP_CIPHER *cipher, aes_256_args_t args)
107103
return ret;
108104
}
109105

110-
/* _decrypt_with_cipher decrypts @in with the OpenSSL cipher specified by
111-
* @cipher.
106+
/* _decrypt_with_cipher decrypts @in with the specified OpenSSL cipher.
107+
* @cipher is a usable EVP_CIPHER, or NULL if early initialization failed.
108+
* @cipher_description is a human-readable description used when reporting deferred errors from initialization, required
109+
* if @cipher might be NULL.
112110
* @key is the input key. @iv is the input IV.
113111
* @out is the output plaintext. @out must be allocated by the caller with
114112
* enough room for the plaintext.
115113
* @bytes_written is the number of bytes that were written to @out.
116114
* Returns false and sets @status on error. @status is required. */
117-
static bool _decrypt_with_cipher(const EVP_CIPHER *cipher, aes_256_args_t args) {
118-
EVP_CIPHER_CTX *ctx;
119-
bool ret = false;
120-
int intermediate_bytes_written = 0;
121-
mongocrypt_status_t *status = args.status;
122-
123-
ctx = EVP_CIPHER_CTX_new();
124-
BSON_ASSERT(ctx);
125-
126-
BSON_ASSERT_PARAM(cipher);
115+
static bool _decrypt_with_cipher(const EVP_CIPHER *cipher, const char *cipher_description, aes_256_args_t args) {
127116
BSON_ASSERT(args.iv);
128117
BSON_ASSERT(args.key);
129118
BSON_ASSERT(args.in);
130119
BSON_ASSERT(args.out);
120+
BSON_ASSERT(args.in->len <= INT_MAX);
121+
122+
mongocrypt_status_t *status = args.status;
123+
if (!cipher) {
124+
BSON_ASSERT_PARAM(cipher_description);
125+
CLIENT_ERR("failed to initialize cipher %s", cipher_description);
126+
return false;
127+
}
128+
131129
BSON_ASSERT((uint32_t)EVP_CIPHER_iv_length(cipher) == args.iv->len);
132130
BSON_ASSERT((uint32_t)EVP_CIPHER_key_length(cipher) == args.key->len);
133-
BSON_ASSERT(args.in->len <= INT_MAX);
131+
132+
bool ret = false;
133+
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
134+
BSON_ASSERT(ctx);
134135

135136
if (!EVP_DecryptInit_ex(ctx, cipher, NULL /* engine */, args.key->data, args.iv->data)) {
136137
CLIENT_ERR("error in EVP_DecryptInit_ex: %s", ERR_error_string(ERR_get_error(), NULL));
@@ -142,6 +143,7 @@ static bool _decrypt_with_cipher(const EVP_CIPHER *cipher, aes_256_args_t args)
142143

143144
*args.bytes_written = 0;
144145

146+
int intermediate_bytes_written = 0;
145147
if (!EVP_DecryptUpdate(ctx, args.out->data, &intermediate_bytes_written, args.in->data, (int)args.in->len)) {
146148
CLIENT_ERR("error in EVP_DecryptUpdate: %s", ERR_error_string(ERR_get_error(), NULL));
147149
goto done;
@@ -165,18 +167,186 @@ static bool _decrypt_with_cipher(const EVP_CIPHER *cipher, aes_256_args_t args)
165167
return ret;
166168
}
167169

170+
bool _native_crypto_random(_mongocrypt_buffer_t *out, uint32_t count, mongocrypt_status_t *status) {
171+
BSON_ASSERT_PARAM(out);
172+
BSON_ASSERT(count <= INT_MAX);
173+
174+
int ret = RAND_bytes(out->data, (int)count);
175+
/* From man page: "RAND_bytes() and RAND_priv_bytes() return 1 on success, -1
176+
* if not supported by the current RAND method, or 0 on other failure. The
177+
* error code can be obtained by ERR_get_error(3)" */
178+
if (ret == -1) {
179+
CLIENT_ERR("secure random IV not supported: %s", ERR_error_string(ERR_get_error(), NULL));
180+
return false;
181+
} else if (ret == 0) {
182+
CLIENT_ERR("failed to generate random IV: %s", ERR_error_string(ERR_get_error(), NULL));
183+
return false;
184+
}
185+
return true;
186+
}
187+
188+
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
189+
// Newest libcrypto support: requires EVP_MAC_CTX_dup and EVP_CIPHER_fetch added in OpenSSL 3.0.0
190+
191+
static struct {
192+
EVP_MAC_CTX *hmac_sha2_256;
193+
EVP_MAC_CTX *hmac_sha2_512;
194+
EVP_CIPHER *aes_256_cbc;
195+
EVP_CIPHER *aes_256_ctr;
196+
EVP_CIPHER *aes_256_ecb; // For testing only
197+
} _mongocrypt_libcrypto;
198+
199+
EVP_MAC_CTX *_build_hmac_ctx_prototype(const char *digest_name) {
200+
EVP_MAC *hmac = EVP_MAC_fetch(NULL, OSSL_MAC_NAME_HMAC, NULL);
201+
if (!hmac) {
202+
return NULL;
203+
}
204+
205+
EVP_MAC_CTX *ctx = EVP_MAC_CTX_new(hmac);
206+
EVP_MAC_free(hmac);
207+
if (!ctx) {
208+
return NULL;
209+
}
210+
211+
OSSL_PARAM params[] = {OSSL_PARAM_construct_utf8_string(OSSL_MAC_PARAM_DIGEST, (char *)digest_name, 0),
212+
OSSL_PARAM_construct_end()};
213+
214+
if (EVP_MAC_CTX_set_params(ctx, params)) {
215+
return ctx;
216+
} else {
217+
EVP_MAC_CTX_free(ctx);
218+
return NULL;
219+
}
220+
}
221+
222+
/* _hmac_with_ctx_prototype computes an HMAC of @in using an OpenSSL context duplicated from @ctx_prototype.
223+
* @ctx_description is a human-readable description used when reporting deferred errors from initialization, required
224+
* if @ctx_prototype might be NULL.
225+
* @key is the input key.
226+
* @out is the output. @out must be allocated by the caller with
227+
* the exact length for the output. E.g. for HMAC 256, @out->len must be 32.
228+
* Returns false and sets @status on error. @status is required. */
229+
static bool _hmac_with_ctx_prototype(const EVP_MAC_CTX *ctx_prototype,
230+
const char *ctx_description,
231+
const _mongocrypt_buffer_t *key,
232+
const _mongocrypt_buffer_t *in,
233+
_mongocrypt_buffer_t *out,
234+
mongocrypt_status_t *status) {
235+
BSON_ASSERT_PARAM(key);
236+
BSON_ASSERT_PARAM(in);
237+
BSON_ASSERT_PARAM(out);
238+
BSON_ASSERT(key->len <= INT_MAX);
239+
240+
if (!ctx_prototype) {
241+
BSON_ASSERT_PARAM(ctx_description);
242+
CLIENT_ERR("failed to initialize algorithm %s", ctx_description);
243+
return false;
244+
}
245+
246+
EVP_MAC_CTX *ctx = EVP_MAC_CTX_dup(ctx_prototype);
247+
if (ctx) {
248+
bool ok = EVP_MAC_init(ctx, key->data, key->len, NULL) && EVP_MAC_update(ctx, in->data, in->len)
249+
&& EVP_MAC_final(ctx, out->data, NULL, out->len);
250+
EVP_MAC_CTX_free(ctx);
251+
if (ok) {
252+
return true;
253+
}
254+
}
255+
CLIENT_ERR("HMAC error: %s", ERR_error_string(ERR_get_error(), NULL));
256+
return false;
257+
}
258+
259+
void _native_crypto_init(void) {
260+
// Early lookup of digest and cipher algorithms avoids both the lookup overhead itself and the overhead of lock
261+
// contention in the default OSSL_LIB_CTX.
262+
//
263+
// Failures now will store NULL, reporting a client error later.
264+
//
265+
// On HMAC fetching:
266+
//
267+
// Note that libcrypto sets an additional trap for us regarding MAC algorithms. An early fetch of the HMAC itself
268+
// won't actually pre-fetch the subalgorithm. The name of the inner digest gets stored as a string, and re-fetched
269+
// when setting up MAC context parameters. To fetch both the outer and inner algorithms ahead of time, we construct
270+
// a prototype EVP_MAC_CTX that can be duplicated before each use.
271+
//
272+
// On thread safety:
273+
//
274+
// This creates objects that are intended to be immutable shared data after initialization. To understand whether
275+
// this is safe we could consult the OpenSSL documentation but currently it's lacking in specifics about the
276+
// individual API functions and types. It offers some general guidelines: "Objects are thread-safe as long as the
277+
// API's being invoked don't modify the object; in this case the parameter is usually marked in the API as C<const>.
278+
// Not all parameters are marked this way." By inspection, we can see that pre-fetched ciphers and MACs are designed
279+
// with atomic reference counting support and appear to be intended for safe immutable use. Contexts are normally
280+
// not safe to share, but these used only as a source for EVP_MAC_CTX_dup() can be treated as immutable.
281+
//
282+
// TODO: This could be refactored to live in mongocrypt_t rather than in global data. Currently there's no way to
283+
// avoid leaking this set of one-time allocations.
284+
//
285+
// TODO: Higher performance yet could be achieved by re-using thread local EVP_MAC_CTX, but this requires careful
286+
// lifecycle management to avoid leaking data. Alternatively, the libmongocrypt API could be modified to include
287+
// some non-shared but long-lived context suitable for keeping these crypto objects. Alternatively still, it may be
288+
// worth using a self contained SHA2 HMAC with favorable performance and portability characteristics.
289+
290+
_mongocrypt_libcrypto.aes_256_cbc = EVP_CIPHER_fetch(NULL, "AES-256-CBC", NULL);
291+
_mongocrypt_libcrypto.aes_256_ctr = EVP_CIPHER_fetch(NULL, "AES-256-CTR", NULL);
292+
_mongocrypt_libcrypto.aes_256_ecb = EVP_CIPHER_fetch(NULL, "AES-256-ECB", NULL);
293+
_mongocrypt_libcrypto.hmac_sha2_256 = _build_hmac_ctx_prototype(OSSL_DIGEST_NAME_SHA2_256);
294+
_mongocrypt_libcrypto.hmac_sha2_512 = _build_hmac_ctx_prototype(OSSL_DIGEST_NAME_SHA2_512);
295+
_native_crypto_initialized = true;
296+
}
297+
168298
bool _native_crypto_aes_256_cbc_encrypt(aes_256_args_t args) {
169-
return _encrypt_with_cipher(EVP_aes_256_cbc(), args);
299+
return _encrypt_with_cipher(_mongocrypt_libcrypto.aes_256_cbc, "AES-256-CBC", args);
170300
}
171301

172302
bool _native_crypto_aes_256_cbc_decrypt(aes_256_args_t args) {
173-
return _decrypt_with_cipher(EVP_aes_256_cbc(), args);
303+
return _decrypt_with_cipher(_mongocrypt_libcrypto.aes_256_cbc, "AES-256-CBC", args);
174304
}
175305

176306
bool _native_crypto_aes_256_ecb_encrypt(aes_256_args_t args); // -Wmissing-prototypes: for testing only.
177307

178308
bool _native_crypto_aes_256_ecb_encrypt(aes_256_args_t args) {
179-
return _encrypt_with_cipher(EVP_aes_256_ecb(), args);
309+
return _encrypt_with_cipher(_mongocrypt_libcrypto.aes_256_ecb, "AES-256-ECB", args);
310+
}
311+
312+
bool _native_crypto_aes_256_ctr_encrypt(aes_256_args_t args) {
313+
return _encrypt_with_cipher(_mongocrypt_libcrypto.aes_256_ctr, "AES-256-CTR", args);
314+
}
315+
316+
bool _native_crypto_aes_256_ctr_decrypt(aes_256_args_t args) {
317+
return _decrypt_with_cipher(_mongocrypt_libcrypto.aes_256_ctr, "AES-256-CTR", args);
318+
}
319+
320+
bool _native_crypto_hmac_sha_256(const _mongocrypt_buffer_t *key,
321+
const _mongocrypt_buffer_t *in,
322+
_mongocrypt_buffer_t *out,
323+
mongocrypt_status_t *status) {
324+
return _hmac_with_ctx_prototype(_mongocrypt_libcrypto.hmac_sha2_256, "HMAC-SHA2-256", key, in, out, status);
325+
}
326+
327+
bool _native_crypto_hmac_sha_512(const _mongocrypt_buffer_t *key,
328+
const _mongocrypt_buffer_t *in,
329+
_mongocrypt_buffer_t *out,
330+
mongocrypt_status_t *status) {
331+
return _hmac_with_ctx_prototype(_mongocrypt_libcrypto.hmac_sha2_512, "HMAC-SHA2-512", key, in, out, status);
332+
}
333+
334+
#else /* OPENSSL_VERSION_NUMBER < 0x30000000L */
335+
// Support for previous libcrypto versions, without early fetch optimization.
336+
337+
#if OPENSSL_VERSION_NUMBER < 0x10100000L || (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x20700000L)
338+
static HMAC_CTX *HMAC_CTX_new(void) {
339+
return bson_malloc0(sizeof(HMAC_CTX));
340+
}
341+
342+
static void HMAC_CTX_free(HMAC_CTX *ctx) {
343+
HMAC_CTX_cleanup(ctx);
344+
bson_free(ctx);
345+
}
346+
#endif
347+
348+
void _native_crypto_init(void) {
349+
_native_crypto_initialized = true;
180350
}
181351

182352
/* _hmac_with_hash computes an HMAC of @in with the OpenSSL hash specified by
@@ -235,37 +405,26 @@ static bool _hmac_with_hash(const EVP_MD *hash,
235405
#endif
236406
}
237407

238-
bool _native_crypto_hmac_sha_512(const _mongocrypt_buffer_t *key,
239-
const _mongocrypt_buffer_t *in,
240-
_mongocrypt_buffer_t *out,
241-
mongocrypt_status_t *status) {
242-
return _hmac_with_hash(EVP_sha512(), key, in, out, status);
408+
bool _native_crypto_aes_256_cbc_encrypt(aes_256_args_t args) {
409+
return _encrypt_with_cipher(EVP_aes_256_cbc(), NULL, args);
243410
}
244411

245-
bool _native_crypto_random(_mongocrypt_buffer_t *out, uint32_t count, mongocrypt_status_t *status) {
246-
BSON_ASSERT_PARAM(out);
247-
BSON_ASSERT(count <= INT_MAX);
412+
bool _native_crypto_aes_256_cbc_decrypt(aes_256_args_t args) {
413+
return _decrypt_with_cipher(EVP_aes_256_cbc(), NULL, args);
414+
}
248415

249-
int ret = RAND_bytes(out->data, (int)count);
250-
/* From man page: "RAND_bytes() and RAND_priv_bytes() return 1 on success, -1
251-
* if not supported by the current RAND method, or 0 on other failure. The
252-
* error code can be obtained by ERR_get_error(3)" */
253-
if (ret == -1) {
254-
CLIENT_ERR("secure random IV not supported: %s", ERR_error_string(ERR_get_error(), NULL));
255-
return false;
256-
} else if (ret == 0) {
257-
CLIENT_ERR("failed to generate random IV: %s", ERR_error_string(ERR_get_error(), NULL));
258-
return false;
259-
}
260-
return true;
416+
bool _native_crypto_aes_256_ecb_encrypt(aes_256_args_t args); // -Wmissing-prototypes: for testing only.
417+
418+
bool _native_crypto_aes_256_ecb_encrypt(aes_256_args_t args) {
419+
return _encrypt_with_cipher(EVP_aes_256_ecb(), NULL, args);
261420
}
262421

263422
bool _native_crypto_aes_256_ctr_encrypt(aes_256_args_t args) {
264-
return _encrypt_with_cipher(EVP_aes_256_ctr(), args);
423+
return _encrypt_with_cipher(EVP_aes_256_ctr(), NULL, args);
265424
}
266425

267426
bool _native_crypto_aes_256_ctr_decrypt(aes_256_args_t args) {
268-
return _decrypt_with_cipher(EVP_aes_256_ctr(), args);
427+
return _decrypt_with_cipher(EVP_aes_256_ctr(), NULL, args);
269428
}
270429

271430
bool _native_crypto_hmac_sha_256(const _mongocrypt_buffer_t *key,
@@ -275,4 +434,13 @@ bool _native_crypto_hmac_sha_256(const _mongocrypt_buffer_t *key,
275434
return _hmac_with_hash(EVP_sha256(), key, in, out, status);
276435
}
277436

437+
bool _native_crypto_hmac_sha_512(const _mongocrypt_buffer_t *key,
438+
const _mongocrypt_buffer_t *in,
439+
_mongocrypt_buffer_t *out,
440+
mongocrypt_status_t *status) {
441+
return _hmac_with_hash(EVP_sha512(), key, in, out, status);
442+
}
443+
444+
#endif /* OPENSSL_VERSION_NUMBER */
445+
278446
#endif /* MONGOCRYPT_ENABLE_CRYPTO_LIBCRYPTO */

0 commit comments

Comments
 (0)