]> source.dussan.org Git - rspamd.git/commitdiff
[Feature] Add tooling to encrypt strings in Lua
authorIvan Stakhov <50211739+LeftTry@users.noreply.github.com>
Thu, 5 Sep 2024 10:48:22 +0000 (13:48 +0300)
committerGitHub <noreply@github.com>
Thu, 5 Sep 2024 10:48:22 +0000 (11:48 +0100)
* [Fix] Provide support for OpenSSL 3.0

* [Feature] Provide function to encode header with configured public key

* [Feature] Provide function to decode header with configured public key

* [Test] Add tests for maybe encode/decode header

* [Minor] Fix tests for encode/decode header

* [Minor] Small clean up

* [Minor] Small clean up

* [Minor] Small fix for OpenSSL 3.0 support

* [Minor] Provide logging

* [Minor] Small fix

* [Fix] Fix typo error

* [Fix] Another typo

* [Minor] Little clean up

* [Minor] Little fix

* [Minor] Small fix

* [Minor] Small fix

* [Minor] Rewrite the arguments of secretbox:encrypt/decrypt functions to a more understandable format

* [Fix] Fix problem with nonce was not provided

* [Test] Add test for nonce

* [Minor] Little clean up

* [Minor] Little clean up

* [Test] Test

* [Test] Test

* [Test] Test

* [Minor] Little fix

* [Minor] Small fix

* [Minor] Small fix

* [Test] Small fix

* [Test] Test

* [Test] Test

* [Test] Test

* [Test] Test

* [Minor] Small fix for fips provider

* [Minor] Change provider apply logic

* [Test] Little fix for provider

* [Minor] Provide OpenSSL <3.0 support

* [Test] Possible provider fix

* [Test] Possible provider fix

* [Test] Little fix

* [Minor] Fix provider issue

* [Minor] Small clean up

* [Minor] Change logging errors

* Update lualib/lua_util.lua

---------

Co-authored-by: Vsevolod Stakhov <vsevolod@rspamd.com>
lualib/lua_util.lua
src/lua/lua_cryptobox.c
test/lua/unit/lua_util.maybe_encrypt_decrypt_header.lua [new file with mode: 0644]

index 470925b959eaca68aadec05c2e39b71fbdd447ee..ffc07842eb71e5561caed5577bc1c0fb813dc048 100644 (file)
@@ -1291,6 +1291,84 @@ exports.maybe_obfuscate_string = function(subject, settings, prefix)
   return subject
 end
 
+---[[[
+-- @function lua_util.maybe_encrypt_header(header, settings, prefix)
+-- Encode header with configured public key if enabled in settings.
+-- If header is not set then nil is returned. If pub_key is empty then header is returned.
+-- Supported settings:
+-- * <prefix>_encrypt = false - no need for encryption of a header
+-- * <prefix>_key = 'key' - key that is used encrypt header
+-- * <prefix>_nonce = 'nonce' - nonce to encrypt header(optional)
+-- @return encrypted header
+---]]]
+exports.maybe_encrypt_header = function(header, settings, prefix)
+  local rspamd_secretbox = require "rspamd_cryptobox_secretbox"
+
+  if not header or header == '' then
+    logger.errx(rspamd_config, "Header: %s is empty or nil", header)
+    return nil
+  elseif settings[prefix .. '_encrypt'] then
+    local key = settings[prefix .. '_key']
+    if not key or key == '' then
+      logger.errx(rspamd_config, "Key: %s is empty or nil", key)
+      return header
+    end
+    local cryptobox = rspamd_secretbox.create(key)
+
+    local nonce = settings[prefix .. '_nonce']
+    local encrypted_header = ''
+    if not nonce or nonce == '' then
+      encrypted_header, nonce = cryptobox:encrypt(header)
+    else
+      encrypted_header = cryptobox:encrypt(header, nonce)
+    end
+    return encrypted_header, nonce
+  end
+end
+
+---[[[
+-- @function lua_util.maybe_decrypt_header(header, settings, prefix, nonce)
+-- Decode enoced with configured public_key header if enabled in settings.
+-- If encoded header is not set then nil is returned. If pub_key is empty then encoded header is returned.
+-- Supported settings:
+-- * <prefix>_encrypt = false - no need for decryption of a header
+-- * <prefix>_key = 'key' - key that is used decrypt header
+-- * <prefix>_nonce = 'nonce' - nonce used to encrypt header(optional)
+-- Nonce is an optional argument if <prefix>_nonce is provided, otherwise it is an required argument
+-- and <prefix>_nonce is an optional
+-- @return decrypted header
+---]]]
+exports.maybe_decrypt_header = function(encrypted_header, settings, prefix, nonce)
+  local rspamd_secretbox = require "rspamd_cryptobox_secretbox"
+
+  if not encrypted_header or encrypted_header == '' then
+    logger.errx(rspamd_config, "Encoded header: %s is empty or nil")
+    return nil
+  elseif settings[prefix .. '_encrypt'] then
+    local key = settings[prefix .. '_key']
+    if not key or key == '' then
+      logger.errx(rspamd_config, "Key: %s is empty or nil")
+      return encrypted_header
+    end
+    local cryptobox = rspamd_secretbox.create(key)
+
+    local result = false
+    local header = ''
+    if not nonce then
+      result, header = cryptobox:decrypt(encrypted_header, settings[prefix .. '_nonce'])
+    else
+      result, header = cryptobox:decrypt(encrypted_header, nonce)
+    end
+
+    if not result then
+      logger.infox(rspamd_config, "Decryption is failed with result: %s and decrypted header: %s", result, header)
+      return nil
+    end
+
+    return header
+  end
+end
+
 ---[[[
 -- @function lua_util.callback_from_string(str)
 -- Converts a string like `return function(...) end` to lua function and return true and this function
index 1b9074f58f56a3b9aff6b5e2b69c1239ba08ad4e..fbd44cecd5749fb97dc3231dba4478fe00bb1a1f 100644 (file)
@@ -37,7 +37,9 @@
 
 #include <stdalign.h>
 #include <openssl/hmac.h>
-
+#if OPENSSL_VERSION_MAJOR >= 3
+#include <openssl/provider.h>
+#endif
 
 enum lua_cryptobox_hash_type {
        LUA_CRYPTOBOX_HASH_BLAKE2 = 0,
@@ -54,7 +56,11 @@ struct rspamd_lua_cryptobox_hash {
        union {
                rspamd_cryptobox_hash_state_t *h;
                EVP_MD_CTX *c;
+#if OPENSSL_VERSION_MAJOR >= 3
+               EVP_MAC_CTX *hmac_c;
+#else
                HMAC_CTX *hmac_c;
+#endif
                rspamd_cryptobox_fast_hash_state_t *fh;
        } content;
 
@@ -899,7 +905,11 @@ rspamd_lua_hash_update(struct rspamd_lua_cryptobox_hash *h,
                        EVP_DigestUpdate(h->content.c, p, len);
                        break;
                case LUA_CRYPTOBOX_HASH_HMAC:
+#if OPENSSL_VERSION_MAJOR >= 3
+                       EVP_MAC_update(h->content.hmac_c, p, len);
+#else
                        HMAC_Update(h->content.hmac_c, p, len);
+#endif
                        break;
                case LUA_CRYPTOBOX_HASH_XXHASH64:
                case LUA_CRYPTOBOX_HASH_XXHASH32:
@@ -930,8 +940,12 @@ lua_cryptobox_hash_dtor(struct rspamd_lua_cryptobox_hash *h)
        (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x30500000)
                HMAC_CTX_cleanup(h->content.hmac_c);
                g_free(h->content.hmac_c);
+#else
+#if OPENSSL_VERSION_MAJOR >= 3
+               EVP_MAC_CTX_free(h->content.hmac_c);
 #else
                HMAC_CTX_free(h->content.hmac_c);
+#endif
 #endif
        }
        else if (h->type == LUA_CRYPTOBOX_HASH_BLAKE2) {
@@ -984,25 +998,53 @@ rspamd_lua_ssl_hmac_create(struct rspamd_lua_cryptobox_hash *h, const EVP_MD *ht
                                                   bool insecure)
 {
        h->type = LUA_CRYPTOBOX_HASH_HMAC;
+       OSSL_PROVIDER *dflt = OSSL_PROVIDER_load(NULL, "default");
+
+#if OPENSSL_VERSION_NUMBER > 0x10100000L
+       if (insecure) {
+               /* Should never ever be used for crypto/security purposes! */
+#ifdef EVP_MD_CTX_FLAG_NON_FIPS_ALLOW
+#if OPENSSL_VERSION_MAJOR >= 3
+               OSSL_PROVIDER *fips = OSSL_PROVIDER_load(NULL, "fips");
+#endif
+       }
+#endif
+#endif
 
 #if OPENSSL_VERSION_NUMBER < 0x10100000L || \
        (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x30500000)
        h->content.hmac_c = g_malloc0(sizeof(*h->content.hmac_c));
+#else
+#if OPENSSL_VERSION_MAJOR >= 3
+       EVP_MAC* mac = EVP_MAC_fetch(NULL, "HMAC", NULL);
+       h->content.hmac_c = EVP_MAC_CTX_new(mac);
+       EVP_MAC_free(mac);
 #else
        h->content.hmac_c = HMAC_CTX_new();
 #endif
-       h->out_len = EVP_MD_size(htype);
+#endif
 
 #if OPENSSL_VERSION_NUMBER > 0x10100000L
        if (insecure) {
                /* Should never ever be used for crypto/security purposes! */
 #ifdef EVP_MD_CTX_FLAG_NON_FIPS_ALLOW
+#if OPENSSL_VERSION_MAJOR < 3
                HMAC_CTX_set_flags(h->content.hmac_c, EVP_MD_CTX_FLAG_NON_FIPS_ALLOW);
+#endif
 #endif
        }
 #endif
 
+       h->out_len = EVP_MD_size(htype);
+#if OPENSSL_VERSION_MAJOR >= 3
+       OSSL_PARAM params[2];
+       params[0] = OSSL_PARAM_construct_utf8_string("digest", EVP_MD_get0_name(htype), 0);
+       params[1] = OSSL_PARAM_construct_end();
+
+       EVP_MAC_init(h->content.hmac_c, key, keylen, params);
+#else
        HMAC_Init_ex(h->content.hmac_c, key, keylen, htype, NULL);
+#endif
 }
 
 static struct rspamd_lua_cryptobox_hash *
@@ -1385,15 +1427,22 @@ lua_cryptobox_hash_reset(lua_State *L)
                        rspamd_cryptobox_hash_init(h->content.h, NULL, 0);
                        break;
                case LUA_CRYPTOBOX_HASH_SSL:
-                       EVP_DigestInit(h->content.c, EVP_MD_CTX_md(h->content.c));
+                       EVP_DigestInit(h->content.c, EVP_MD_CTX_get0_md(h->content.c));
                        break;
                case LUA_CRYPTOBOX_HASH_HMAC:
 #if OPENSSL_VERSION_NUMBER < 0x10100000L || \
        (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x30500000)
                        /* Old openssl is awesome... */
                        HMAC_Init_ex(h->content.hmac_c, NULL, 0, h->content.hmac_c->md, NULL);
+#else
+#if OPENSSL_VERSION_MAJOR >= 3
+                       EVP_MAC_CTX_free(h->content.hmac_c);
+                       EVP_MAC *mac = EVP_MAC_fetch(NULL, "HMAC", NULL);
+                       h->content.hmac_c = EVP_MAC_CTX_new(mac);
+                       EVP_MAC_free(mac);
 #else
                        HMAC_CTX_reset(h->content.hmac_c);
+#endif
 #endif
                        break;
                case LUA_CRYPTOBOX_HASH_XXHASH64:
@@ -1452,7 +1501,13 @@ lua_cryptobox_hash_finish(struct rspamd_lua_cryptobox_hash *h)
                memcpy(h->out, out, ssl_outlen);
                break;
        case LUA_CRYPTOBOX_HASH_HMAC:
+#if OPENSSL_VERSION_MAJOR >= 3
+               size_t ssl_outlen_size_t = ssl_outlen;
+               EVP_MAC_final(h->content.hmac_c, out, &ssl_outlen_size_t, sizeof(out));
+               ssl_outlen = ssl_outlen_size_t;
+#else
                HMAC_Final(h->content.hmac_c, out, &ssl_outlen);
+#endif
                h->out_len = ssl_outlen;
                g_assert(ssl_outlen <= sizeof(h->out));
                memcpy(h->out, out, ssl_outlen);
@@ -2469,31 +2524,31 @@ lua_cryptobox_gen_dkim_keypair(lua_State *L)
                EVP_PKEY *pk;
 
                e = BN_new();
-               r = RSA_new();
                pk = EVP_PKEY_new();
 
                if (BN_set_word(e, RSA_F4) != 1) {
                        BN_free(e);
-                       RSA_free(r);
                        EVP_PKEY_free(pk);
 
                        return luaL_error(L, "BN_set_word failed");
                }
 
-               if (RSA_generate_key_ex(r, nbits, e, NULL) != 1) {
+               EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
+               if (EVP_PKEY_keygen_init(pctx) != 1) {
                        BN_free(e);
-                       RSA_free(r);
                        EVP_PKEY_free(pk);
+                       EVP_PKEY_CTX_free(pctx);
 
-                       return luaL_error(L, "RSA_generate_key_ex failed");
+                       return luaL_error(L, "EVP_PKEY_keygen_init failed");
                }
-
-               if (EVP_PKEY_set1_RSA(pk, r) != 1) {
+               EVP_PKEY_CTX_set_rsa_keygen_bits(pctx, nbits);
+               EVP_PKEY_CTX_set1_rsa_keygen_pubexp(pctx, e);
+               if (EVP_PKEY_keygen(pctx, &pk) != 1) {
                        BN_free(e);
-                       RSA_free(r);
                        EVP_PKEY_free(pk);
+                       EVP_PKEY_CTX_free(pctx);
 
-                       return luaL_error(L, "EVP_PKEY_set1_RSA failed");
+                       return luaL_error(L, "EVP_PKEY_keygen failed");
                }
 
                BIO *mbio;
@@ -2505,12 +2560,11 @@ lua_cryptobox_gen_dkim_keypair(lua_State *L)
                mbio = BIO_new(BIO_s_mem());
 
                /* Process private key */
-               rc = i2d_RSAPrivateKey_bio(mbio, r);
+               rc = i2d_PrivateKey_bio(mbio, pk);
 
                if (rc == 0) {
                        BIO_free(mbio);
                        BN_free(e);
-                       RSA_free(r);
                        EVP_PKEY_free(pk);
 
                        return luaL_error(L, "i2d_RSAPrivateKey_bio failed");
@@ -2528,12 +2582,11 @@ lua_cryptobox_gen_dkim_keypair(lua_State *L)
 
                /* Process public key */
                BIO_reset(mbio);
-               rc = i2d_RSA_PUBKEY_bio(mbio, r);
+               rc = i2d_PUBKEY_bio(mbio, pk);
 
                if (rc == 0) {
                        BIO_free(mbio);
                        BN_free(e);
-                       RSA_free(r);
                        EVP_PKEY_free(pk);
 
                        return luaL_error(L, "i2d_RSA_PUBKEY_bio failed");
@@ -2550,7 +2603,6 @@ lua_cryptobox_gen_dkim_keypair(lua_State *L)
                pub_out->flags = RSPAMD_TEXT_FLAG_OWN;
 
                BN_free(e);
-               RSA_free(r);
                EVP_PKEY_free(pk);
                BIO_free(mbio);
        }
@@ -2717,7 +2769,7 @@ lua_cryptobox_secretbox_encrypt(lua_State *L)
        struct rspamd_lua_text *out;
 
        if (sbox == NULL) {
-               return luaL_error(L, "invalid arguments");
+               return luaL_error(L, "invalid argument for secretbox state");
        }
 
        if (lua_isstring(L, 2)) {
@@ -2727,14 +2779,14 @@ lua_cryptobox_secretbox_encrypt(lua_State *L)
                struct rspamd_lua_text *t = lua_check_text(L, 2);
 
                if (!t) {
-                       return luaL_error(L, "invalid arguments; userdata is not text");
+                       return luaL_error(L, "invalid first argument; userdata is not text");
                }
 
                in = t->start;
                inlen = t->len;
        }
        else {
-               return luaL_error(L, "invalid arguments; userdata or string are expected");
+               return luaL_error(L, "invalid first argument; userdata or string are expected");
        }
 
        /* Nonce part */
@@ -2746,14 +2798,14 @@ lua_cryptobox_secretbox_encrypt(lua_State *L)
                        struct rspamd_lua_text *t = lua_check_text(L, 3);
 
                        if (!t) {
-                               return luaL_error(L, "invalid arguments; userdata is not text");
+                               return luaL_error(L, "invalid second argument; userdata is not text");
                        }
 
                        nonce = t->start;
                        nlen = t->len;
                }
                else {
-                       return luaL_error(L, "invalid arguments; userdata or string are expected");
+                       return luaL_error(L, "invalid second argument; userdata or string are expected");
                }
 
                if (nlen < 1 || nlen > crypto_secretbox_NONCEBYTES) {
@@ -2791,8 +2843,8 @@ lua_cryptobox_secretbox_encrypt(lua_State *L)
 /***
  * @method rspamd_cryptobox_secretbox:decrypt(input, nonce)
  * Decrypts data using secretbox
- * @param {string/text} nonce nonce used to encrypt
  * @param {string/text} input input to decrypt
+ * @param {string/text} nonce nonce used to encrypt
  * @param {table} params optional parameters - NYI
  * @return {boolean},{rspamd_text} decryption result + decrypted text
  */
@@ -2806,7 +2858,7 @@ lua_cryptobox_secretbox_decrypt(lua_State *L)
        struct rspamd_lua_text *out;
 
        if (sbox == NULL) {
-               return luaL_error(L, "invalid arguments");
+               return luaL_error(L, "invalid argument for secretbox state");
        }
 
        /* Input argument */
@@ -2817,14 +2869,14 @@ lua_cryptobox_secretbox_decrypt(lua_State *L)
                struct rspamd_lua_text *t = lua_check_text(L, 2);
 
                if (!t) {
-                       return luaL_error(L, "invalid arguments; userdata is not text");
+                       return luaL_error(L, "invalid first argument; userdata is not text");
                }
 
                in = t->start;
                inlen = t->len;
        }
        else {
-               return luaL_error(L, "invalid arguments; userdata or string are expected");
+               return luaL_error(L, "invalid first argument; userdata or string are expected");
        }
 
        /* Nonce argument */
@@ -2835,14 +2887,14 @@ lua_cryptobox_secretbox_decrypt(lua_State *L)
                struct rspamd_lua_text *t = lua_check_text(L, 3);
 
                if (!t) {
-                       return luaL_error(L, "invalid arguments; userdata is not text");
+                       return luaL_error(L, "invalid second argument; userdata is not text");
                }
 
                nonce = t->start;
                nlen = t->len;
        }
        else {
-               return luaL_error(L, "invalid arguments; userdata or string are expected");
+               return luaL_error(L, "invalid second argument; userdata or string are expected");
        }
 
 
diff --git a/test/lua/unit/lua_util.maybe_encrypt_decrypt_header.lua b/test/lua/unit/lua_util.maybe_encrypt_decrypt_header.lua
new file mode 100644 (file)
index 0000000..6131010
--- /dev/null
@@ -0,0 +1,57 @@
+local util = require 'lua_util'
+
+context("Lua util - maybe encrypt/decrypt header", function()
+    test("Encrypt/Decrypt header with nonce", function()
+        local header = tostring('X-Spamd-Result')
+        local settings = {
+            prefix = 'prefix',
+            prefix_encrypt = true,
+            prefix_key = 'key',
+            prefix_nonce = 'nonce'
+        }
+
+        local encrypted_header = util.maybe_encrypt_header(header, settings, settings.prefix)
+        if encrypted_header == header or encrypted_header == nil then
+            assert_true(false, 'Failed to encrypt header')
+        end
+
+        local decrypted_header = util.maybe_decrypt_header(encrypted_header, settings, settings.prefix)
+        if decrypted_header == encrypted_header or decrypted_header == nil then
+            assert_true(false, 'Failed to decrypt header')
+        end
+
+        if tostring(header) == tostring(decrypted_header) then
+            assert_true(true, 'Succeed to confirm equality of original header and decrypted header')
+        else
+            assert_rspamd_table_eq_sorted({actual = { decrypted_header },
+                                           expect = { header }})
+        end
+    end)
+
+    test("Encrypt/Decrypt header without nonce", function()
+        local header = tostring('X-Spamd-Result')
+        local settings = {
+            prefix = 'prefix',
+            prefix_encrypt = true,
+            prefix_key = 'key'
+        }
+
+        local encrypted_header, nonce = util.maybe_encrypt_header(header, settings, settings.prefix)
+        if encrypted_header == header or encrypted_header == nil then
+            assert_true(false, 'Failed to encrypt header')
+        end
+
+        local decrypted_header = util.maybe_decrypt_header(encrypted_header, settings,
+                settings.prefix, nonce)
+        if decrypted_header == encrypted_header or decrypted_header == nil then
+            assert_true(false, 'Failed to decrypt header')
+        end
+
+        if tostring(header) == tostring(decrypted_header) then
+            assert_true(true, 'Succeed to confirm equality of original header and decrypted header')
+        else
+            assert_rspamd_table_eq_sorted({actual = { decrypted_header },
+                                           expect = { header }})
+        end
+    end)
+end)
\ No newline at end of file