MEDIUM: ssl: Add local passphrase cache

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.
This commit is contained in:
Remi Tricot-Le Breton 2025-10-28 18:00:46 +01:00 committed by William Lallemand
parent 478dd7bad0
commit 5a036d223b
2 changed files with 187 additions and 8 deletions

View 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);

View 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 */