]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MEDIUM: ssl: Add local passphrase cache
authorRemi Tricot-Le Breton <rlebreton@haproxy.com>
Tue, 28 Oct 2025 17:00:46 +0000 (18:00 +0100)
committerWilliam Lallemand <wlallemand@haproxy.com>
Wed, 29 Oct 2025 09:54:17 +0000 (10:54 +0100)
Instead of calling the external password command for all loaded
encrypted certificates, we will keep a local password cache.
The passwords won't be stored as plain text, they will be stored
obfuscated into the password cache. The obfuscation is simply based on a
XOR'ing with a random number built during init.
After init is performed, the password cache is overwritten and freed so
that no dangling info allowing to dump the passwords remains.

src/ssl_ckch.c
src/ssl_sock.c

index 0427d099374d2caacadbc1aa0ab454d0cbe33788..942537dd7f5aab978a491d86bffebf2b9049e321 100644 (file)
@@ -613,8 +613,17 @@ int ssl_sock_load_key_into_ckch(const char *path, char *buf, struct ckch_data *d
                        goto end;
        }
 
-       /* Read Private Key */
-       key = PEM_read_bio_PrivateKey(in, NULL, ssl_sock_passwd_cb, &cb_data);
+       /* Read Private Key
+        * Since multiple private keys might have different passphrases that are
+        * stored in a local cache, we want to try all the already known
+        * passphrases first before raising an error. The passphrase_idx field
+        * of the cb_data parameter will be modified in the callback and set to
+        * -1 after the external passphrase tool is called.
+        */
+       do {
+               key = PEM_read_bio_PrivateKey(in, NULL, ssl_sock_passwd_cb, &cb_data);
+       } while (!key && cb_data.passphrase_idx != -1);
+
        if (key == NULL) {
                memprintf(err, "%sunable to load private key from file '%s'.\n",
                          err && *err ? *err : "", path);
index 00de17afea1fe7c03275d977fca9824636d510f7..a664d6d3585ea4381ca57ed8ab0e2f34bc5baf8a 100644 (file)
@@ -3653,6 +3653,128 @@ out:
        return cfgerr;
 }
 
+#define DFLT_PASSPHRASES_CNT 4
+static struct ist *passphrase_cache;
+static uint64_t *passphrase_randoms;
+static int passphrase_cache_size;
+static int passphrase_idx;
+
+/*
+ * Create a passphrase cache of size DFLT_PASSPHRASES_CNT and a random number
+ * cache of the same size. Each entry of the passphrase cache will be obfuscated
+ * thanks to the corresponding entry in the random cache.
+ */
+static int ssl_sock_create_passphrase_cache(void)
+{
+       int idx;
+
+       passphrase_cache_size = DFLT_PASSPHRASES_CNT;
+       passphrase_cache = calloc(passphrase_cache_size,  sizeof(*passphrase_cache));
+
+       if (!passphrase_cache)
+               goto end;
+
+       passphrase_randoms = calloc(passphrase_cache_size, sizeof(*passphrase_randoms));
+
+       if (!passphrase_randoms)
+               goto end;
+
+       for (idx = 0; idx < passphrase_cache_size; ++idx) {
+               passphrase_randoms[idx] = ha_random64();
+       }
+
+       return 0;
+
+end:
+       ha_alert("ssl_sock_passwd_cb: passphrase data allocation failure");
+       ha_free(&passphrase_cache);
+       ha_free(&passphrase_randoms);
+       passphrase_cache_size = 0;
+
+       return -1;
+}
+
+/*
+ * When the passphrase cache is too small we resize it to double its size.
+ */
+static void ssl_sock_resize_passphrase_cache(void)
+{
+       int idx;
+       int new_size = passphrase_cache_size << 1;
+
+       passphrase_randoms = realloc(passphrase_randoms, sizeof(*passphrase_randoms) * (new_size));
+       if (!passphrase_randoms) {
+               ha_alert("ssl_sock_passwd_cb: passphrase randoms realloc failed");
+               passphrase_idx = -1;
+               passphrase_cache_size = 0;
+       } else {
+               /* Create the new randoms in the second half of the random
+                * cache.
+                */
+               for (idx = passphrase_cache_size; idx < new_size; ++idx) {
+                       passphrase_randoms[idx] = ha_random64();
+               }
+       }
+
+       if (passphrase_cache_size) {
+               passphrase_cache_size = new_size;
+               passphrase_cache = realloc(passphrase_cache, sizeof(*passphrase_cache) * passphrase_cache_size);
+               if (!passphrase_cache) {
+                       ha_alert("ssl_sock_passwd_cb: passphrase cache realloc failed");
+                       passphrase_idx = -1;
+                       passphrase_cache_size = 0;
+               }
+       }
+}
+
+static int ssl_sock_clear_passphrase_cache(void)
+{
+       int idx = 0;
+
+       if (passphrase_cache) {
+               for (; idx < passphrase_cache_size; ++idx) {
+                       if (!isttest(passphrase_cache[idx]))
+                               break;
+
+                       /* Erase stored passphrases just in case some memory
+                        * ends up leaking */
+                       memset(passphrase_cache[idx].ptr, 0, passphrase_cache[idx].len);
+                       istfree(&passphrase_cache[idx]);
+               }
+               ha_free(&passphrase_cache);
+               passphrase_cache_size = 0;
+               passphrase_idx = -1;
+       }
+
+       ha_free(&passphrase_randoms);
+
+       return 0;
+}
+
+REGISTER_POST_CHECK(ssl_sock_clear_passphrase_cache);
+
+/*
+ * Obfuscate cleartext passphrases by XORing it with an 8 bytes random number.
+ * The same random will be used for the whole string.
+ */
+static void obfuscate_str(char *string, int len, uint8_t *random, int random_len)
+{
+       int idx = 0;
+       int subidx = 0;
+
+       while (idx < len) {
+
+               for (subidx = 0; subidx < random_len && idx < len ; ++subidx, ++idx) {
+                       string[idx] ^= random[subidx];
+               }
+       }
+}
+
+static inline void deobfuscate_str(char *string, int len, uint8_t *random, int random_len)
+{
+       obfuscate_str(string, len, random, random_len);
+}
+
 /*
  * Certificate password callback. The password will be provided by the external
  * program defined in global section (see 'ssl-passphrase-cmd'). It will be
@@ -3660,19 +3782,20 @@ out:
  */
 int ssl_sock_passwd_cb(char *buf, int size, int rwflag, void *userdata)
 {
-       int pass_len;
+       int pass_len = 0;
        int read_len;
        pid_t pid = -1;
        int wstatus = 0;
-
        int fd[2];
+       char *bufstart = buf;
 
        struct passphrase_cb_data *data = userdata;
 
-       if (!data)
+       if (!data || data->passphrase_idx == -1)
                return -1;
 
        if (!global_ssl.passphrase_cmd) {
+               data->passphrase_idx = -1;
                ha_alert("Trying to load a passphrase-protected private key without an 'ssl-passphrase-cmd' defined.");
                return -1;
        }
@@ -3689,6 +3812,31 @@ int ssl_sock_passwd_cb(char *buf, int size, int rwflag, void *userdata)
                return -1;
        }
 
+       if (!passphrase_cache)
+               if (ssl_sock_create_passphrase_cache())
+                       return -1;
+
+       /* Try all the already known passphrases first. */
+       if (data->passphrase_idx < passphrase_idx) {
+               struct ist tmp = istdup(passphrase_cache[data->passphrase_idx]);
+               int length = 0;
+               if (isttest(tmp)) {
+                       deobfuscate_str(istptr(tmp), istlen(tmp),
+                                       (uint8_t *)&passphrase_randoms[data->passphrase_idx],
+                                       sizeof(passphrase_randoms[data->passphrase_idx]));
+                       strncpy(buf, istptr(tmp), size);
+                       length = istlen(tmp);
+                       istfree(&tmp);
+
+                       ++data->passphrase_idx;
+                       return MIN(length, size);
+               }
+       }
+
+       /* If we get to this point, we must have tried all the known passphrases
+        * without success, we must call the external passphrase command. */
+       data->passphrase_idx = -1;
+
        if (pipe(fd) < 0) {
                ha_alert("ssl_sock_passwd_cb: pipe error");
                return -1;
@@ -3703,7 +3851,7 @@ int ssl_sock_passwd_cb(char *buf, int size, int rwflag, void *userdata)
        case 0:
                /* In child process, need to call external tool via execv to get
                 * passphrase */
-               close(0);
+               dup2(devnullfd, 0);
                dup2(fd[1], 1);
 
                execvp(global_ssl.passphrase_cmd[0], global_ssl.passphrase_cmd);
@@ -3714,11 +3862,33 @@ int ssl_sock_passwd_cb(char *buf, int size, int rwflag, void *userdata)
                /* Close write side of pipe, it won't be used by the parent */
                close(fd[1]);
 
-               while (1) {
+               while (size > 0) {
                        read_len = read(fd[0], buf, size);
                        if (read_len <= 0)
                                break;
-                       pass_len = read_len;
+                       buf += read_len;
+                       size -= read_len;
+                       pass_len += read_len;
+               }
+               if (pass_len) {
+                       /* Copy passphrase in passphrase "cache" (after
+                        * obfuscating it). */
+                       if (passphrase_idx == passphrase_cache_size - 1) {
+                               ssl_sock_resize_passphrase_cache();
+                       }
+                       if (passphrase_idx >= 0) {
+                               passphrase_cache[passphrase_idx] = istdup(ist2(bufstart, pass_len));
+                               if (!isttest(passphrase_cache[passphrase_idx])) {
+                                       ha_alert("ssl_sock_passwd_cb: passphrase strdup failed");
+                                       passphrase_idx = -1;
+                               } else {
+                                       obfuscate_str(istptr(passphrase_cache[passphrase_idx]),
+                                                     istlen(passphrase_cache[passphrase_idx]),
+                                                     (uint8_t *)&passphrase_randoms[passphrase_idx],
+                                                     sizeof(passphrase_randoms[passphrase_idx]));
+                                       ++passphrase_idx;
+                               }
+                       }
                }
 
                /* Close read side of pipe */