]> git.ipfire.org Git - thirdparty/openssl.git/commitdiff
Add test for BIO password callback functionality
authorerbsland-dev <github@erbsland.dev>
Thu, 29 Aug 2024 21:08:46 +0000 (23:08 +0200)
committerTomas Mraz <tomas@openssl.org>
Mon, 9 Sep 2024 07:04:28 +0000 (09:04 +0200)
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)

(cherry picked from commit fa6ae88a47a37678e8f8567ec2622bef515ac286)

test/bio_pw_callback_test.c [new file with mode: 0644]
test/build.info
test/recipes/61-test_bio_pw_callback.t [new file with mode: 0644]
test/recipes/61-test_bio_pw_callback_data/private_key.pem [new file with mode: 0644]

diff --git a/test/bio_pw_callback_test.c b/test/bio_pw_callback_test.c
new file mode 100644 (file)
index 0000000..56c0cea
--- /dev/null
@@ -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);
+}
index d182de0f93f43b718ae17d3f0f5eead02fd355a8..4ba6392cefc0cf12f78a14d65307fda1ba4facd6 100644 (file)
@@ -61,7 +61,7 @@ IF[{- !$disabled{tests} -}]
           keymgmt_internal_test hexstr_test provider_status_test defltfips_test \
           bio_readbuffer_test user_property_test pkcs7_test upcallstest \
           provfetchtest prov_config_test rand_test fips_version_test \
-          nodefltctxtest
+          nodefltctxtest bio_pw_callback_test
 
   IF[{- !$disabled{'deprecated-3.0'} -}]
     PROGRAMS{noinst}=enginetest
@@ -962,6 +962,10 @@ ENDIF
     DEPEND[timing_load_creds]=../libcrypto.a
   ENDIF
 
+  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;
diff --git a/test/recipes/61-test_bio_pw_callback.t b/test/recipes/61-test_bio_pw_callback.t
new file mode 100644 (file)
index 0000000..4cb1db1
--- /dev/null
@@ -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");
diff --git a/test/recipes/61-test_bio_pw_callback_data/private_key.pem b/test/recipes/61-test_bio_pw_callback_data/private_key.pem
new file mode 100644 (file)
index 0000000..f9c9ae5
--- /dev/null
@@ -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-----