Add test for BIO password callback functionality

Related to #8441

This commit introduces a test suite for the password callback mechanism used when reading or writing encrypted and PEM or DER encoded keys via a BIO in OpenSSL. The test is designed to cover various edge cases, particularly focusing on scenarios where the password callback might return unexpected or malformed data from user code.

By simulating different callback behaviors, including negative returns, zero-length passwords, passwords that exactly fill the buffer and wrongly reported lengths. Also testing for the correct behaviour of binary passwords that contain a null byte in the middle.

Reviewed-by: Dmitry Belyavskiy <beldmit@gmail.com>
Reviewed-by: Tomas Mraz <tomas@openssl.org>
(Merged from https://github.com/openssl/openssl/pull/25330)
This commit is contained in:
erbsland-dev 2024-08-29 23:08:46 +02:00 committed by Tomas Mraz
parent 7845ff7692
commit fa6ae88a47
4 changed files with 474 additions and 1 deletions

419
test/bio_pw_callback_test.c Normal file
View File

@ -0,0 +1,419 @@
/*
* Copyright 2024 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
#include "testutil.h"
#include <openssl/bio.h>
#include <openssl/pem.h>
/* dummy data that needs to be passed to the callback */
typedef struct CallbackData {
int dummy;
} CALLBACK_DATA;
/* constants */
static char *key_password = "weak_password";
static int key_password_len = 13;
static char *a0a_password = "aaaaaaaa\0aaaaaaaa";
static int a0a_password_len = 17;
static char *a0b_password = "aaaaaaaa\0bbbbbbbb";
static int a0b_password_len = 17;
/* shared working data for all tests */
static char *key_file = NULL;
static EVP_PKEY *original_pkey = NULL;
static BUF_MEM *encrypted_key_data = NULL;
static int encrypted_key_data_size = 0;
static BIO *bio = NULL;
static EVP_PKEY *pkey = NULL;
static CALLBACK_DATA *callback_data = NULL;
static int callback_ret = 0;
/* the test performed by the callback */
typedef enum CallbackTest {
CB_TEST_NEGATIVE = 0,
CB_TEST_ZERO_LENGTH,
CB_TEST_WEAK,
CB_TEST_16ZERO,
CB_TEST_A0A,
CB_TEST_A0B,
CB_TEST_MATCH_SIZE,
CB_TEST_EXCEED_SIZE
} CALLBACK_TEST;
static CALLBACK_TEST callback_test = CB_TEST_NEGATIVE;
typedef enum KeyEncoding {
KE_PEM = 0,
KE_PKCS8
} KEY_ENCODING;
typedef enum ExpectedResult {
ER_FAILURE = 0,
ER_SUCCESS
} EXPECTED_RESULT;
typedef enum OPTION_choice {
OPT_ERR = -1,
OPT_EOF = 0,
OPT_KEY_FILE,
OPT_TEST_ENUM
} OPTION_CHOICE;
const OPTIONS *test_get_options(void)
{
static const OPTIONS test_options[] = {
OPT_TEST_OPTIONS_DEFAULT_USAGE,
{ "keyfile", OPT_KEY_FILE, '<',
"The PEM file with the encrypted key to load" },
{ NULL }
};
return test_options;
}
static void cleanup_after_test(void)
{
free(encrypted_key_data);
encrypted_key_data = NULL;
encrypted_key_data_size = 0;
BIO_free(bio);
bio = NULL;
EVP_PKEY_free(pkey);
pkey = NULL;
}
static int callback_copy_password(char *buf, int size)
{
int ret = -1;
switch (callback_test) {
case CB_TEST_NEGATIVE:
break;
case CB_TEST_ZERO_LENGTH:
ret = 0;
break;
case CB_TEST_WEAK:
memcpy(buf, key_password, key_password_len);
ret = key_password_len;
break;
case CB_TEST_16ZERO:
memset(buf, 0, 16);
ret = 16;
break;
case CB_TEST_A0A:
memcpy(buf, a0a_password, a0a_password_len);
ret = a0a_password_len;
break;
case CB_TEST_A0B:
memcpy(buf, a0b_password, a0b_password_len);
ret = a0b_password_len;
break;
case CB_TEST_MATCH_SIZE:
memset(buf, 'e', size);
ret = size;
break;
case CB_TEST_EXCEED_SIZE:
memset(buf, 'e', size);
ret = 1000000;
break;
}
return ret;
}
static int read_callback(char *buf, int size, int rwflag, void *u)
{
int ret = -1;
/* basic verification of the received data */
if (!TEST_ptr_eq(u, callback_data))
goto err;
if (!TEST_ptr(buf))
goto err;
if (!TEST_int_gt(size, 0))
goto err;
if (!TEST_int_eq(rwflag, 0))
goto err;
ret = callback_copy_password(buf, size);
callback_ret = 1;
err:
return ret;
}
static int write_callback(char *buf, int size, int rwflag, void *u)
{
int ret = -1;
/* basic verification of the received data */
if (!TEST_ptr_eq(u, callback_data))
goto err;
if (!TEST_ptr(buf))
goto err;
if (!TEST_int_gt(size, 0))
goto err;
if (!TEST_int_eq(rwflag, 1))
goto err;
ret = callback_copy_password(buf, size);
callback_ret = 1;
err:
return ret;
}
static int re_encrypt_key(KEY_ENCODING key_encoding)
{
int w_ret = 0;
int ret = 0;
BUF_MEM *bptr = NULL;
free(encrypted_key_data);
encrypted_key_data = NULL;
encrypted_key_data_size = 0;
if (!TEST_ptr(bio = BIO_new(BIO_s_mem())))
goto err;
callback_ret = 0;
switch (key_encoding) {
case KE_PEM:
w_ret = PEM_write_bio_PrivateKey(bio, original_pkey,
EVP_aes_256_cbc(),
NULL, 0, write_callback,
callback_data);
break;
case KE_PKCS8:
w_ret = i2d_PKCS8PrivateKey_bio(bio, original_pkey, EVP_aes_256_cbc(),
NULL, 0, write_callback,
callback_data);
break;
}
if (!TEST_int_ne(w_ret, 0))
goto err;
if (!TEST_int_eq(callback_ret, 1))
goto err;
encrypted_key_data_size = BIO_get_mem_data(bio, &encrypted_key_data);
BIO_get_mem_ptr(bio, &bptr);
if (!BIO_set_close(bio, BIO_NOCLOSE))
goto err;
bptr->data = NULL;
ret = 1;
err:
BUF_MEM_free(bptr);
BIO_free(bio);
bio = NULL;
return ret;
}
static int decrypt_key(KEY_ENCODING key_encoding,
EXPECTED_RESULT expected_result)
{
EVP_PKEY *r_ret = NULL;
int ret = 0;
if (!TEST_ptr(bio = BIO_new_mem_buf(encrypted_key_data,
encrypted_key_data_size)))
goto err;
EVP_PKEY_free(pkey);
pkey = NULL;
callback_ret = 0;
switch (key_encoding) {
case KE_PEM:
r_ret = PEM_read_bio_PrivateKey(bio, &pkey, read_callback,
callback_data);
break;
case KE_PKCS8:
r_ret = d2i_PKCS8PrivateKey_bio(bio, &pkey, read_callback,
callback_data);
break;
}
if (expected_result == ER_SUCCESS) {
if (!TEST_ptr(r_ret))
goto err;
} else {
if (!TEST_ptr_null(r_ret))
goto err;
}
if (!TEST_int_eq(callback_ret, 1))
goto err;
ret = 1;
err:
EVP_PKEY_free(pkey);
pkey = NULL;
BIO_free(bio);
bio = NULL;
return ret;
}
static int full_cycle_test(KEY_ENCODING key_encoding, CALLBACK_TEST write_test,
CALLBACK_TEST read_test,
EXPECTED_RESULT expected_read_result)
{
int ret = 0;
callback_test = write_test;
callback_ret = 0;
if (!re_encrypt_key(key_encoding))
goto err;
if (!TEST_int_eq(callback_ret, 1))
goto err;
callback_test = read_test;
if (!decrypt_key(key_encoding, expected_read_result))
goto err;
if (!TEST_int_eq(callback_ret, 1))
goto err;
ret = 1;
err:
cleanup_after_test();
return ret;
}
static int test_pem_negative(void)
{
return full_cycle_test(KE_PEM, CB_TEST_WEAK, CB_TEST_NEGATIVE, ER_FAILURE);
}
static int test_pem_zero_length(void)
{
return full_cycle_test(KE_PEM, CB_TEST_ZERO_LENGTH, CB_TEST_ZERO_LENGTH,
ER_SUCCESS);
}
static int test_pem_weak(void)
{
return full_cycle_test(KE_PEM, CB_TEST_WEAK, CB_TEST_WEAK, ER_SUCCESS);
}
static int test_pem_16zero(void)
{
return full_cycle_test(KE_PEM, CB_TEST_16ZERO, CB_TEST_16ZERO, ER_SUCCESS);
}
static int test_pem_a0a(void)
{
return full_cycle_test(KE_PEM, CB_TEST_A0A, CB_TEST_A0A, ER_SUCCESS);
}
static int test_pem_a0a_a0b(void)
{
return full_cycle_test(KE_PEM, CB_TEST_A0A, CB_TEST_A0B, ER_FAILURE);
}
static int test_pem_match_size(void)
{
return full_cycle_test(KE_PEM, CB_TEST_MATCH_SIZE, CB_TEST_MATCH_SIZE,
ER_SUCCESS);
}
static int test_pem_exceed_size(void)
{
return full_cycle_test(KE_PEM, CB_TEST_MATCH_SIZE, CB_TEST_EXCEED_SIZE,
ER_FAILURE);
}
static int test_pkcs8_negative(void)
{
return full_cycle_test(KE_PKCS8, CB_TEST_WEAK, CB_TEST_NEGATIVE, ER_FAILURE);
}
static int test_pkcs8_zero_length(void)
{
return full_cycle_test(KE_PKCS8, CB_TEST_ZERO_LENGTH, CB_TEST_ZERO_LENGTH,
ER_SUCCESS);
}
static int test_pkcs8_weak(void)
{
return full_cycle_test(KE_PKCS8, CB_TEST_WEAK, CB_TEST_WEAK, ER_SUCCESS);
}
static int test_pkcs8_16zero(void)
{
return full_cycle_test(KE_PKCS8, CB_TEST_16ZERO, CB_TEST_16ZERO,
ER_SUCCESS);
}
static int test_pkcs8_a0a(void)
{
return full_cycle_test(KE_PKCS8, CB_TEST_A0A, CB_TEST_A0A, ER_SUCCESS);
}
static int test_pkcs8_a0a_a0b(void)
{
return full_cycle_test(KE_PKCS8, CB_TEST_A0A, CB_TEST_A0B, ER_FAILURE);
}
static int test_pkcs8_match_size(void)
{
return full_cycle_test(KE_PKCS8, CB_TEST_MATCH_SIZE, CB_TEST_MATCH_SIZE,
ER_SUCCESS);
}
static int test_pkcs8_exceed_size(void)
{
return full_cycle_test(KE_PKCS8, CB_TEST_MATCH_SIZE, CB_TEST_EXCEED_SIZE,
ER_FAILURE);
}
static int callback_original_pw(char *buf, int size, int rwflag, void *u)
{
memcpy(buf, key_password, key_password_len);
return key_password_len;
}
int setup_tests(void)
{
OPTION_CHOICE o;
while ((o = opt_next()) != OPT_EOF) {
switch (o) {
case OPT_KEY_FILE:
key_file = opt_arg();
break;
case OPT_TEST_CASES:
break;
default:
case OPT_ERR:
return 0;
}
}
/* create dummy callback data for verification */
callback_data = OPENSSL_malloc(sizeof(CALLBACK_DATA));
memset(callback_data, 0, sizeof(CALLBACK_DATA));
/* read the original key */
if (!TEST_ptr(bio = BIO_new_file(key_file, "r")))
return 0;
if (!TEST_ptr(PEM_read_bio_PrivateKey(bio, &original_pkey,
callback_original_pw, NULL)))
return 0;
BIO_free(bio);
bio = NULL;
/* add all tests */
ADD_TEST(test_pem_negative);
ADD_TEST(test_pem_zero_length);
ADD_TEST(test_pem_weak);
ADD_TEST(test_pem_16zero);
ADD_TEST(test_pem_a0a);
ADD_TEST(test_pem_a0a_a0b);
ADD_TEST(test_pem_match_size);
ADD_TEST(test_pem_exceed_size);
ADD_TEST(test_pkcs8_negative);
ADD_TEST(test_pkcs8_zero_length);
ADD_TEST(test_pkcs8_weak);
ADD_TEST(test_pkcs8_16zero);
ADD_TEST(test_pkcs8_a0a);
ADD_TEST(test_pkcs8_a0a_a0b);
ADD_TEST(test_pkcs8_match_size);
ADD_TEST(test_pkcs8_exceed_size);
return 1;
}
void cleanup_tests(void)
{
BUF_MEM_free(encrypted_key_data);
OPENSSL_free(callback_data);
EVP_PKEY_free(original_pkey);
}

View File

@ -64,7 +64,7 @@ IF[{- !$disabled{tests} -}]
ca_internals_test bio_tfo_test membio_test bio_dgram_test list_test \
fips_version_test x509_test hpke_test pairwise_fail_test \
nodefltctxtest evp_xof_test x509_load_cert_file_test bio_meth_test \
x509_acert_test x509_req_test strtoultest
x509_acert_test x509_req_test strtoultest bio_pw_callback_test
IF[{- !$disabled{'rpk'} -}]
PROGRAMS{noinst}=rpktest
@ -1224,6 +1224,10 @@ ENDIF
INCLUDE[strtoultest]=../include ../apps/include
DEPEND[strtoultest]=../libcrypto libtestutil.a
SOURCE[bio_pw_callback_test]=bio_pw_callback_test.c
INCLUDE[bio_pw_callback_test]=../include ../apps/include
DEPEND[bio_pw_callback_test]=../libcrypto libtestutil.a
{-
use File::Spec::Functions;
use File::Basename;

View File

@ -0,0 +1,20 @@
#! /usr/bin/env perl
# Copyright 2024 The OpenSSL Project Authors. All Rights Reserved.
#
# Licensed under the Apache License 2.0 (the "License"). You may not use
# this file except in compliance with the License. You can obtain a copy
# in the file LICENSE in the source distribution or at
# https://www.openssl.org/source/license.html
use strict;
use warnings;
use OpenSSL::Test qw(:DEFAULT data_file);
setup('test_bio_pw_callback');
plan tests => 1;
my $private_key_path = data_file("private_key.pem");
ok(run(test(["bio_pw_callback_test", "-keyfile", $private_key_path])),
"Running bio_pw_callback_test");

View File

@ -0,0 +1,30 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQmftpln/ZNiEznncq
+u0FuwICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEBO5TGcD0mGTfRS8
HgafEXYEggTQOasEXPm4ChGPzfXACYhaAtMFnfL9qpI1S30bHMUHsWuXLZDFPNty
7KNKWr35woaq3XFEeul7onszcBBRrRwPkTqOifuv/J01s7oS0uC6jwbvSkAFNjHe
jkgvMMQA3y7nwZ2wSwVjO2K91qasTjNivus3ZaCvGqGpgNckEXILPZJEdWteWP+1
SN9zLxxeHwgt5SrMfylrTghLB8b119/uq4GnOYHZdhMbp4YmneuGqvlZ7nle7qLY
33tuM5deajk9hINLfbYWGwURaOZ+r++Rvrz4OxISfe70uXT+2fcSZPVkNT5a6B5T
9rCwdF69W/+3au50gfc2VEF/xZBajxLI0PBpMSpxNE3a5/3YLKXAs+z0YJdQKNhN
U+SpOUv8D2GraJVfP7MddO2JvETh8w7tGN/a8qSw07Z91SE3Vfuq0l5PheC/vXJq
/xxU3YSbZC7LCSZn1aXBlj9KbTh2o1ARzdJsVYo1xY2OIFtFpncOjQDuaAmsNcZE
CuB9FUcBwwO/bjooIkv4lJU+DWDxrCR7Si8PZ4hHgXCXXKiXA20SBccUYm0Z4HR3
i2tm9UTwAuCy1BF7hRmPLIyvlgtlKh2V9Cre5j86GoKTmPh/q5DHdSmNAM8Aakct
GdQgscOXRmHq7/1nec28wEhlbqVyYJ45MZbWhBTrycMru/ch9+ZnsIgPXLfbBA+P
6GHK1DF+onKZtMkH0SNMU3X1arlJKRreVQsvkbgL7aw3mI0veYa4/tJUf7hbkPpA
LArQU5wQ+A9mzC+tYMfz3mrIE05FrpYkHRxiB/odeNvCTMR7DhGoghhnYUN/gSSN
qH5EBG2hQ/pJ5ZSawE+P9+vCLlvcc4n00zgi0s3rMN2AntPZoI3sWKZcbbgJoOIH
cbAmBAKCIiwmlPmI0hjEAIXRBixJzHVGNowuSc3jy5pIiSjmDESnARl+n5imqI3D
po9OuCHpo4nRLcAX0GrJqqKxUG+R1A8g/AooIGEPQgkXk/4v9gwd4aBvwT4YxR44
onAXdyBMM0T8C+8dUmT6OPvU5w6JHFidJfhBgJhDIdj9JM+wWdr1CW94todjEyKY
Xe3NRG1bGbcN6HBVwbe4UZ39A9p4kKGyiXexlsD+DvFxwaGvSy2rp0lLabz19Kkr
fnLU1Ugb38AnEYTGYJMB9nO19lHW62Mk6+9ky42x8X9vBn81Nif/c0kmvEKsZEfw
UM7m0fIWTZOWSH01DGIXqCoCk7vJ1CSm0wUsAvyKFLm1qnM5eJJNMlBbayDDBsnU
Jj9hx7GWjujVKFwFngUOoFpmFWB72bqeBWenaQJhIVydQa1rolny0TECJIkFOsUK
Wa0y52V4h68Ig5G5p2WHG0RlEVtmcgzSoL1mLE5UdOYaH5oB7nTVM+Z0b8HJFrYc
7Xhym8uNq6UHc4Ae6TT8EA3lA3fDttedKzWxlBFXqX9behl2uBnPzCl3cS2G2Uek
xtexjecZINP8L5i6eIL7bPoVMF5CUsUhIWFA0gzIovRBRvVS91HnTrIDLvqF8YgQ
ToctUU/vS8r3x2/TIR60UBvW0vkoFa+lfzHtsxBnT1nMBZNeeHOCM8QtboyI9Ir9
UkJbTO+QpJQ5A3ELharpcqr7iywDOnLSV9LZSUZr934zOrRl2oAXx/0=
-----END ENCRYPTED PRIVATE KEY-----