[Openvpn-devel,v4,16+17/18] Add a unit test for external key provider

Message ID 20220120161616.13447-1-selva.nair@gmail.com
State Accepted
Headers show
Series [Openvpn-devel,v4,16+17/18] Add a unit test for external key provider | expand

Commit Message

Selva Nair Jan. 20, 2022, 5:16 a.m. UTC
From: Selva Nair <selva.nair@gmail.com>

Tests:
- Check SIGNATURE and KEYMGMT methods can be fetched
  from the provider
- Load sample RSA and EC keys as management-external-key
  and check that their sign callbacks are correctly exercised:
  with and without digest support mocked in the client
  capability flag.
 -Test generic key load and signature

v4: 16/18 and 17/18 of v3 squashed into one patch

Signed-off-by: Selva Nair <selva.nair@gmail.com>
---
 tests/unit_tests/openvpn/Makefile.am     |  16 +
 tests/unit_tests/openvpn/test_provider.c | 403 +++++++++++++++++++++++
 2 files changed, 419 insertions(+)
 create mode 100644 tests/unit_tests/openvpn/test_provider.c

Comments

Gert Doering Jan. 20, 2022, 8:01 a.m. UTC | #1
Combining Arne's ACK for 16+17 into this one.  As far as I can see
(not checked line-by-line) this is indeed the same code, just squashed
into one commit, not touching configure.ac (thanks).

And indeed, it now tests something :-)

[==========] Running 3 test(s).
[ RUN      ] xkey_provider_test_fetch
[       OK ] xkey_provider_test_fetch
[ RUN      ] xkey_provider_test_mgmt_sign_cb
[       OK ] xkey_provider_test_mgmt_sign_cb
[ RUN      ] xkey_provider_test_generic_sign_cb
[       OK ] xkey_provider_test_generic_sign_cb
[==========] 3 test(s) run.
[  PASSED  ] 3 test(s).
PASS: provider_testdriver

and just an empty "PASS: provider_testdriver" when compiled without
XKEY.

Your patch has been applied to the master branch.

commit 4fe50675946f4533a9a373f4332e417f1bbfeabe
Author: Selva Nair
Date:   Thu Jan 20 11:16:16 2022 -0500

     Add a unit test for external key provider

     Signed-off-by: Selva Nair <selva.nair@gmail.com>
     Acked-by: Arne Schwabe <arne@rfc2549.org>
     Message-Id: <20220120161616.13447-1-selva.nair@gmail.com>
     URL: https://www.mail-archive.com/openvpn-devel@lists.sourceforge.net/msg23608.html
     Signed-off-by: Gert Doering <gert@greenie.muc.de>


--
kind regards,

Gert Doering

Patch

diff --git a/tests/unit_tests/openvpn/Makefile.am b/tests/unit_tests/openvpn/Makefile.am
index 44b77cc5..6b5c94ab 100644
--- a/tests/unit_tests/openvpn/Makefile.am
+++ b/tests/unit_tests/openvpn/Makefile.am
@@ -11,6 +11,8 @@  if HAVE_LD_WRAP_SUPPORT
 test_binaries += tls_crypt_testdriver
 endif
 
+test_binaries += provider_testdriver
+
 TESTS = $(test_binaries)
 check_PROGRAMS = $(test_binaries)
 
@@ -95,6 +97,20 @@  networking_testdriver_SOURCES = test_networking.c mock_msg.c \
 	$(openvpn_srcdir)/platform.c
 endif
 
+provider_testdriver_CFLAGS  = @TEST_CFLAGS@ \
+	-I$(openvpn_includedir) -I$(compat_srcdir) -I$(openvpn_srcdir) \
+	$(OPTIONAL_CRYPTO_CFLAGS)
+provider_testdriver_LDFLAGS = @TEST_LDFLAGS@ \
+	$(OPTIONAL_CRYPTO_LIBS)
+
+provider_testdriver_SOURCES = test_provider.c mock_msg.c \
+	$(openvpn_srcdir)/xkey_helper.c \
+	$(openvpn_srcdir)/xkey_provider.c \
+	$(openvpn_srcdir)/buffer.c \
+	$(openvpn_srcdir)/base64.c \
+	mock_get_random.c \
+	$(openvpn_srcdir)/platform.c
+
 auth_token_testdriver_CFLAGS  = @TEST_CFLAGS@ \
 	-I$(openvpn_includedir) -I$(compat_srcdir) -I$(openvpn_srcdir) \
 	$(OPTIONAL_CRYPTO_CFLAGS)
diff --git a/tests/unit_tests/openvpn/test_provider.c b/tests/unit_tests/openvpn/test_provider.c
new file mode 100644
index 00000000..0182b3b4
--- /dev/null
+++ b/tests/unit_tests/openvpn/test_provider.c
@@ -0,0 +1,403 @@ 
+/*
+ *  OpenVPN -- An application to securely tunnel IP networks
+ *             over a single UDP port, with support for SSL/TLS-based
+ *             session authentication and key exchange,
+ *             packet encryption, packet authentication, and
+ *             packet compression.
+ *
+ *  Copyright (C) 2021 Selva Nair <selva.nair@gmail.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by the
+ *  Free Software Foundation, either version 2 of the License,
+ *  or (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License along
+ *  with this program; if not, write to the Free Software Foundation, Inc.,
+ *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#elif defined(_MSC_VER)
+#include "config-msvc.h"
+#endif
+
+#include "syshead.h"
+#include "manage.h"
+#include "xkey_common.h"
+
+#ifdef HAVE_XKEY_PROVIDER
+
+#include <setjmp.h>
+#include <cmocka.h>
+#include <openssl/bio.h>
+#include <openssl/pem.h>
+#include <openssl/core_names.h>
+#include <openssl/evp.h>
+
+struct management *management; /* global */
+static int mgmt_callback_called;
+
+#ifndef _countof
+#define _countof(x) sizeof((x))/sizeof(*(x))
+#endif
+
+static OSSL_PROVIDER *prov[2];
+
+/* public keys for testing -- RSA and EC */
+static const char * const pubkey1 = "-----BEGIN PUBLIC KEY-----\n"
+    "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7GWP6RLCGlvmVioIqYI6\n"
+    "LUR4owA7sJ/nJxBAk+/xzD6gqgSigBsTqeb+gdZwkKjY1N4w2DUA0r5i8Eja/BWN\n"
+    "xMZtC5nxK4MACtMqIwvlzfk130NhFXKtlZj2cyFBXqDdRyeg1ZrUQagcHVcgcReP\n"
+    "9yiePgfO7NUOQk8edEeOR53SFCgnLBQQ9dGWtZN0hO/5BN6NSm/fd6vq0VjTRP5a\n"
+    "BAH/BnqX9/3jV0jh8N9AE59mI1rjVVQ9VDnuAPkS8dLfdC661/CNxt0YWByTIgt1\n"
+    "+qjW4LUvLbnU/rlPhuJ1SBZg+z/JtDBCKfs7syu5WYFqRvNFg7/91Rr/NwxvW/1h\n"
+    "8QIDAQAB\n"
+    "-----END PUBLIC KEY-----\n";
+
+static const char * const pubkey2 = "-----BEGIN PUBLIC KEY-----\n"
+    "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEO85iXW+HgnUkwlj1DohNVw0GsnGIh1gZ\n"
+    "u95ff1JiUaJIkYNIkZA+hwIPFVH5aJcSCv3SPIeDS2VUAESNKHZJBQ==\n"
+    "-----END PUBLIC KEY-----\n";
+
+static const char *pubkeys[] = {pubkey1, pubkey2};
+
+static const char *prov_name = "ovpn.xkey";
+
+static const char* test_msg = "Lorem ipsum dolor sit amet, consectetur "
+                              "adipisici elit, sed eiusmod tempor incidunt "
+                              "ut labore et dolore magna aliqua.";
+
+static const char* test_msg_b64 =
+    "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2ljaS"
+    "BlbGl0LCBzZWQgZWl1c21vZCB0ZW1wb3IgaW5jaWR1bnQgdXQgbGFib3JlIGV0IGRv"
+    "bG9yZSBtYWduYSBhbGlxdWEu";
+
+/* Sha256 digest of test_msg excluding NUL terminator */
+static const uint8_t test_digest[] =
+    {0x77, 0x38, 0x65, 0x00, 0x1e, 0x96, 0x48, 0xc6, 0x57, 0x0b, 0xae,
+     0xc0, 0xb7, 0x96, 0xf9, 0x66, 0x4d, 0x5f, 0xd0, 0xb7, 0xdb, 0xf3,
+     0x3a, 0xbf, 0x02, 0xcc, 0x78, 0x61, 0x83, 0x20, 0x20, 0xee};
+
+static const char *test_digest_b64 = "dzhlAB6WSMZXC67At5b5Zk1f0Lfb8zq/Asx4YYMgIO4=";
+
+/* Dummy signature used only to check that the expected callback
+ * was successfully exercised. Keep this shorter than 64 bytes
+ * --- the smallest size of the actual signature with the above
+ * keys.
+ */
+static const uint8_t good_sig[] =
+   {0xd8, 0xa7, 0xd9, 0x81, 0xd8, 0xaa, 0xd8, 0xad, 0x20, 0xd9, 0x8a, 0xd8,
+    0xa7, 0x20, 0xd8, 0xb3, 0xd9, 0x85, 0xd8, 0xb3, 0xd9, 0x85, 0x0};
+
+static const char *good_sig_b64 = "2KfZgdiq2K0g2YrYpyDYs9mF2LPZhQA=";
+
+static EVP_PKEY *
+load_pubkey(const char *pem)
+{
+    BIO *in = BIO_new_mem_buf(pem, -1);
+    assert_non_null(in);
+
+    EVP_PKEY *pkey = PEM_read_bio_PUBKEY(in, NULL, NULL, NULL);
+    assert_non_null(pkey);
+
+    BIO_free(in);
+    return pkey;
+}
+
+static void
+init_test()
+{
+    prov[0] = OSSL_PROVIDER_load(NULL,"default");
+    OSSL_PROVIDER_add_builtin(NULL, prov_name, xkey_provider_init);
+    prov[1] = OSSL_PROVIDER_load(NULL, prov_name);
+
+    /* set default propq matching what we use in ssl_openssl.c */
+    EVP_set_default_properties(NULL, "?provider!=ovpn.xkey");
+
+    management = test_calloc(sizeof(*management), 1);
+}
+
+static void
+uninit_test()
+{
+    for (size_t i = 0; i < _countof(prov); i++)
+    {
+        if (prov[i])
+        {
+            OSSL_PROVIDER_unload(prov[i]);
+        }
+    }
+    test_free(management);
+}
+
+/* Mock management callback for signature.
+ * We check that the received data to sign matches test_msg or
+ * test_digest and return a predefined string as signature so that
+ * the caller can validate all steps up to sending the data to
+ * the management client.
+ */
+char *
+management_query_pk_sig(struct management *man, const char *b64_data,
+                        const char *algorithm)
+{
+    char *out = NULL;
+
+    /* indicate entry to the callback */
+    mgmt_callback_called = 1;
+
+    const char *expected_tbs = test_digest_b64;
+    if (strstr(algorithm, "data=message"))
+    {
+         expected_tbs = test_msg_b64;
+         assert_non_null(strstr(algorithm, "hashalg=SHA256"));
+    }
+    assert_string_equal(b64_data, expected_tbs);
+
+    /* We test using ECDSA or PSS with saltlen = digest */
+    if (!strstr(algorithm, "ECDSA"))
+    {
+        assert_non_null(strstr(algorithm, "RSA_PKCS1_PSS_PADDING,hashalg=SHA256,saltlen=digest"));
+    }
+
+    /* Return a predefined string as sig so that the caller
+     * can confirm that this callback was exercised.
+     */
+    out = strdup(good_sig_b64);
+    assert_non_null(out);
+
+    return out;
+}
+
+/* Check signature and keymgmt methods can be fetched from the provider */
+static void
+xkey_provider_test_fetch(void **state)
+{
+    assert_true(OSSL_PROVIDER_available(NULL, prov_name));
+
+    const char *algs[] = {"RSA", "ECDSA"};
+
+    for (size_t i = 0; i < _countof(algs); i++)
+    {
+        EVP_SIGNATURE *sig = EVP_SIGNATURE_fetch(NULL, algs[i], "provider=ovpn.xkey");
+        assert_non_null(sig);
+        assert_string_equal(OSSL_PROVIDER_get0_name(EVP_SIGNATURE_get0_provider(sig)), prov_name);
+
+        EVP_SIGNATURE_free(sig);
+    }
+
+    const char *names[] = {"RSA", "EC"};
+
+    for (size_t i = 0; i < _countof(names); i++)
+    {
+        EVP_KEYMGMT *km = EVP_KEYMGMT_fetch(NULL, names[i], "provider=ovpn.xkey");
+        assert_non_null(km);
+        assert_string_equal(OSSL_PROVIDER_get0_name(EVP_KEYMGMT_get0_provider(km)), prov_name);
+
+        EVP_KEYMGMT_free(km);
+    }
+}
+
+/* sign a test message using pkey -- caller must free the returned sig */
+static uint8_t *
+digest_sign(EVP_PKEY *pkey)
+{
+    uint8_t *sig = NULL;
+    size_t siglen = 0;
+
+    OSSL_PARAM params[6] = {OSSL_PARAM_END};
+
+    const char *mdname = "SHA256";
+    const char *padmode = "pss";
+    const char *saltlen = "digest";
+
+    if (EVP_PKEY_get_id(pkey) == EVP_PKEY_RSA)
+    {
+        params[0] = OSSL_PARAM_construct_utf8_string(OSSL_SIGNATURE_PARAM_DIGEST, (char *)mdname, 0);
+        params[1] = OSSL_PARAM_construct_utf8_string(OSSL_SIGNATURE_PARAM_PAD_MODE, (char *)padmode, 0);
+        params[2] = OSSL_PARAM_construct_utf8_string(OSSL_SIGNATURE_PARAM_PSS_SALTLEN, (char *)saltlen, 0);
+        /* same digest for mgf1 */
+        params[3] = OSSL_PARAM_construct_utf8_string(OSSL_SIGNATURE_PARAM_MGF1_DIGEST, (char *)saltlen, 0);
+        params[4] = OSSL_PARAM_construct_end();
+    }
+
+    EVP_PKEY_CTX *pctx = NULL;
+    EVP_MD_CTX *mctx = EVP_MD_CTX_new();
+
+    if (!mctx
+        || EVP_DigestSignInit_ex(mctx, &pctx, mdname, NULL, NULL, pkey,  params) <= 0)
+    {
+        fail_msg("Failed to initialize EVP_DigestSignInit_ex()");
+        goto done;
+    }
+
+    /* sign with sig = NULL to get required siglen */
+    assert_int_equal(EVP_DigestSign(mctx, sig, &siglen, (uint8_t*)test_msg, strlen(test_msg)), 1);
+    assert_true(siglen > 0);
+
+    if ((sig = test_calloc(1, siglen)) == NULL)
+    {
+        fail_msg("Out of memory");
+    }
+    assert_int_equal(EVP_DigestSign(mctx, sig, &siglen, (uint8_t*)test_msg, strlen(test_msg)), 1);
+
+done:
+    if (mctx)
+    {
+        EVP_MD_CTX_free(mctx); /* pctx is internally allocated and freed by mctx */
+    }
+    return sig;
+}
+
+/* Check loading of management external key and have sign callback exercised
+ * for RSA and EC keys with and without digest support in management client.
+ * Sha256 digest used for both cases with pss padding for RSA.
+ */
+static void
+xkey_provider_test_mgmt_sign_cb(void **state)
+{
+    EVP_PKEY *pubkey;
+    for (size_t i = 0; i < _countof(pubkeys); i++)
+    {
+        pubkey = load_pubkey(pubkeys[i]);
+        assert_true(pubkey != NULL);
+        EVP_PKEY *privkey = xkey_load_management_key(NULL, pubkey);
+        assert_true(privkey != NULL);
+
+        management->settings.flags = MF_EXTERNAL_KEY|MF_EXTERNAL_KEY_PSSPAD;
+
+        /* first without digest support in management client */
+again:
+        mgmt_callback_called = 0;
+        uint8_t *sig = digest_sign(privkey);
+        assert_non_null(sig);
+
+        /* check callback for signature got exercised */
+        assert_int_equal(mgmt_callback_called, 1);
+        assert_memory_equal(sig, good_sig, sizeof(good_sig));
+        test_free(sig);
+
+        if (!(management->settings.flags & MF_EXTERNAL_KEY_DIGEST))
+        {
+            management->settings.flags |= MF_EXTERNAL_KEY_DIGEST;
+            goto again; /* this time with digest support announced */
+        }
+
+        EVP_PKEY_free(pubkey);
+        EVP_PKEY_free(privkey);
+    }
+}
+
+/* helpers for testing generic key load and sign */
+static int xkey_free_called;
+static int xkey_sign_called;
+static void
+xkey_free(void *handle)
+{
+    xkey_free_called = 1;
+    /* We use a dummy string as handle -- check its value */
+    assert_string_equal(handle, "xkey_handle");
+}
+
+static int
+xkey_sign(void *handle, unsigned char *sig, size_t *siglen,
+          const unsigned char *tbs, size_t tbslen, XKEY_SIGALG s)
+{
+    if (!sig)
+    {
+        *siglen = 256; /* some arbitrary size */
+        return 1;
+    }
+
+    xkey_sign_called = 1; /* called with non-null sig */
+
+    if (!strcmp(s.op, "DigestSign"))
+    {
+        assert_memory_equal(tbs, test_msg, strlen(test_msg));
+    }
+    else
+    {
+        assert_memory_equal(tbs, test_digest, sizeof(test_digest));
+    }
+
+    /* For the test use sha256 and PSS padding for RSA */
+    assert_int_equal(OBJ_sn2nid(s.mdname), NID_sha256);
+    if (!strcmp(s.keytype, "RSA"))
+    {
+        assert_string_equal(s.padmode, "pss"); /* we use PSS for the test */
+    }
+    else if (strcmp(s.keytype, "EC"))
+    {
+        fail_msg("Unknown keytype: %s", s.keytype);
+    }
+
+    /* return a predefined string as sig */
+    memcpy(sig, good_sig, min_int(sizeof(good_sig), *siglen));
+
+    return 1;
+}
+
+/* Load a key as a generic key and check its sign op gets
+ * called for signature.
+ */
+static void
+xkey_provider_test_generic_sign_cb(void **state)
+{
+    EVP_PKEY *pubkey;
+    const char *dummy = "xkey_handle"; /* a dummy handle for the external key */
+
+    for (size_t i = 0; i < _countof(pubkeys); i++)
+    {
+        pubkey = load_pubkey(pubkeys[i]);
+        assert_true(pubkey != NULL);
+
+        EVP_PKEY *privkey = xkey_load_generic_key(NULL, (void*)dummy, pubkey, xkey_sign, xkey_free);
+        assert_true(privkey != NULL);
+
+        xkey_sign_called = 0;
+        xkey_free_called = 0;
+        uint8_t *sig = digest_sign(privkey);
+        assert_non_null(sig);
+
+        /* check callback for signature got exercised */
+        assert_int_equal(xkey_sign_called, 1);
+        assert_memory_equal(sig, good_sig, sizeof(good_sig));
+        test_free(sig);
+
+        EVP_PKEY_free(pubkey);
+        EVP_PKEY_free(privkey);
+
+        /* check key's free-op got called */
+        assert_int_equal(xkey_free_called, 1);
+    }
+}
+
+int
+main(void)
+{
+    init_test();
+
+    const struct CMUnitTest tests[] = {
+        cmocka_unit_test(xkey_provider_test_fetch),
+        cmocka_unit_test(xkey_provider_test_mgmt_sign_cb),
+        cmocka_unit_test(xkey_provider_test_generic_sign_cb),
+    };
+
+    int ret = cmocka_run_group_tests_name("xkey provider tests", tests, NULL, NULL);
+
+    uninit_test();
+    return ret;
+}
+#else
+int
+main(void)
+{
+    return 0;
+}
+#endif /* HAVE_XKEY_PROVIDER */