mirror of
https://gitlab.com/openconnect/ocserv.git
synced 2026-10-10 08:12:03 +08:00
Signed-off-by: Dimitri Papadopoulos <3350651-DimitriPapadopoulos@users.noreply.gitlab.com>
1207 lines
29 KiB
C
1207 lines
29 KiB
C
/*
|
|
* Copyright (C) 2013-2018 Nikos Mavrogiannopoulos
|
|
*
|
|
* This file is part of ocserv.
|
|
*
|
|
* ocserv 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.
|
|
*
|
|
* ocserv 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, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <config.h>
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <sys/types.h>
|
|
#include <sys/select.h>
|
|
#include <sys/wait.h>
|
|
#include <sys/stat.h>
|
|
#include <fcntl.h>
|
|
#include <sys/socket.h>
|
|
#include <netdb.h>
|
|
#include <system.h>
|
|
#include <errno.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/un.h>
|
|
#include <common.h>
|
|
#include <main.h>
|
|
#include <sec-mod.h>
|
|
#include <tlslib.h>
|
|
#include <ipc.pb-c.h>
|
|
#include <sec-mod-sup-config.h>
|
|
#include <sec-mod-resume.h>
|
|
#include "gnulib/cloexec.h"
|
|
#include <assert.h>
|
|
|
|
#include <gnutls/gnutls.h>
|
|
#include <gnutls/crypto.h>
|
|
#include <gnutls/abstract.h>
|
|
#include "log.h"
|
|
|
|
#define MAINTENANCE_TIME 310
|
|
|
|
static int need_maintenance;
|
|
static int need_reload;
|
|
static int need_exit;
|
|
|
|
static void reload_server(sec_mod_st *sec);
|
|
|
|
static int load_keys(sec_mod_st *sec, unsigned int force);
|
|
|
|
static int pin_callback(void *user, int attempt, const char *token_url,
|
|
const char *token_label, unsigned int flags, char *pin,
|
|
size_t pin_max)
|
|
{
|
|
struct pin_st *ps = user;
|
|
int srk = 0;
|
|
const char *p;
|
|
unsigned int len;
|
|
|
|
if (flags & GNUTLS_PIN_FINAL_TRY) {
|
|
oc_syslog(
|
|
LOG_ERR,
|
|
"PIN callback: final try before locking; not attempting to unlock");
|
|
return -1;
|
|
}
|
|
|
|
if (flags & GNUTLS_PIN_WRONG) {
|
|
oc_syslog(LOG_ERR,
|
|
"PIN callback: wrong PIN was entered for '%s' (%s)",
|
|
token_label, token_url);
|
|
return -1;
|
|
}
|
|
|
|
if (ps->pin[0] == 0) {
|
|
oc_syslog(LOG_ERR,
|
|
"PIN required for '%s' but pin-file was not set",
|
|
token_label);
|
|
return -1;
|
|
}
|
|
|
|
if (strcmp(token_url, "SRK") == 0 || strcmp(token_label, "SRK") == 0) {
|
|
srk = 1;
|
|
p = ps->srk_pin;
|
|
} else {
|
|
p = ps->pin;
|
|
}
|
|
|
|
if (srk != 0 && ps->srk_pin[0] == 0) {
|
|
oc_syslog(LOG_ERR,
|
|
"PIN required for '%s' but srk-pin-file was not set",
|
|
token_label);
|
|
return -1;
|
|
}
|
|
|
|
len = strlen(p);
|
|
if (len > pin_max - 1) {
|
|
oc_syslog(LOG_ERR, "Too long PIN (%u chars)", len);
|
|
return -1;
|
|
}
|
|
|
|
memcpy(pin, p, len);
|
|
pin[len] = 0;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int load_pins(struct perm_cfg_st *config, struct pin_st *s)
|
|
{
|
|
int fd, ret;
|
|
|
|
s->srk_pin[0] = 0;
|
|
s->pin[0] = 0;
|
|
|
|
if (config->srk_pin_file != NULL) {
|
|
fd = open(config->srk_pin_file, O_RDONLY);
|
|
if (fd < 0) {
|
|
oc_syslog(LOG_ERR, "could not open SRK PIN file '%s'",
|
|
config->srk_pin_file);
|
|
return -1;
|
|
}
|
|
|
|
ret = read(fd, s->srk_pin, sizeof(s->srk_pin) - 1);
|
|
close(fd);
|
|
if (ret <= 1) {
|
|
oc_syslog(LOG_ERR, "could not read from PIN file '%s'",
|
|
config->srk_pin_file);
|
|
return -1;
|
|
}
|
|
|
|
if (s->srk_pin[ret - 1] == '\n' || s->srk_pin[ret - 1] == '\r')
|
|
s->srk_pin[ret - 1] = 0;
|
|
s->srk_pin[ret] = 0;
|
|
}
|
|
|
|
if (config->pin_file != NULL) {
|
|
fd = open(config->pin_file, O_RDONLY);
|
|
if (fd < 0) {
|
|
oc_syslog(LOG_ERR, "could not open PIN file '%s'",
|
|
config->pin_file);
|
|
return -1;
|
|
}
|
|
|
|
ret = read(fd, s->pin, sizeof(s->pin) - 1);
|
|
close(fd);
|
|
if (ret <= 1) {
|
|
oc_syslog(LOG_ERR, "could not read from PIN file '%s'",
|
|
config->pin_file);
|
|
return -1;
|
|
}
|
|
|
|
if (s->pin[ret - 1] == '\n' || s->pin[ret - 1] == '\r')
|
|
s->pin[ret - 1] = 0;
|
|
s->pin[ret] = 0;
|
|
}
|
|
|
|
if (config->key_pin != NULL) {
|
|
strlcpy(s->pin, config->key_pin, sizeof(s->pin));
|
|
}
|
|
|
|
if (config->srk_pin != NULL) {
|
|
strlcpy(s->srk_pin, config->srk_pin, sizeof(s->srk_pin));
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int handle_op(void *pool, int cfd, sec_mod_st *sec, uint8_t type,
|
|
uint8_t *rep, size_t rep_size)
|
|
{
|
|
SecOpMsg msg = SEC_OP_MSG__INIT;
|
|
int ret;
|
|
|
|
msg.data.data = rep;
|
|
msg.data.len = rep_size;
|
|
|
|
ret = send_msg(pool, cfd, type, &msg,
|
|
(pack_size_func)sec_op_msg__get_packed_size,
|
|
(pack_func)sec_op_msg__pack);
|
|
if (ret < 0) {
|
|
seclog(sec, LOG_WARNING, "sec-mod error in sending reply");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int process_worker_packet(void *pool, int cfd, pid_t pid,
|
|
sec_mod_st *sec, cmd_request_t cmd,
|
|
uint8_t *buffer, size_t buffer_size)
|
|
{
|
|
unsigned int i;
|
|
gnutls_datum_t data, out;
|
|
int ret;
|
|
SecOpMsg *op;
|
|
vhost_cfg_st *vhost;
|
|
#if GNUTLS_VERSION_NUMBER >= 0x030600
|
|
unsigned int bits;
|
|
SecGetPkMsg *pkm;
|
|
#endif
|
|
PROTOBUF_ALLOCATOR(pa, pool);
|
|
|
|
seclog(sec, LOG_DEBUG, "cmd [size=%d] %s\n", (int)buffer_size,
|
|
cmd_request_to_str(cmd));
|
|
data.data = buffer;
|
|
data.size = buffer_size;
|
|
|
|
switch (cmd) {
|
|
#if GNUTLS_VERSION_NUMBER >= 0x030600
|
|
case CMD_SEC_GET_PK:
|
|
pkm = sec_get_pk_msg__unpack(&pa, data.size, data.data);
|
|
if (pkm == NULL) {
|
|
seclog(sec, LOG_INFO, "error unpacking sec get pk\n");
|
|
return -1;
|
|
}
|
|
|
|
vhost = find_vhost(sec->vconfig, pkm->vhost);
|
|
/* check for static analyzer - find_vhost is always non-NULL */
|
|
assert(vhost != NULL);
|
|
|
|
i = pkm->key_idx;
|
|
if (i >= vhost->key_size) {
|
|
seclog(sec, LOG_INFO,
|
|
"%sreceived out-of-bounds key index (%d); have %d keys",
|
|
PREFIX_VHOST(vhost), i, vhost->key_size);
|
|
return -1;
|
|
}
|
|
|
|
pkm->pk = gnutls_privkey_get_pk_algorithm(vhost->key[i], &bits);
|
|
pkm->bits = bits;
|
|
|
|
ret = send_msg(pool, cfd, CMD_SEC_GET_PK, pkm,
|
|
(pack_size_func)sec_get_pk_msg__get_packed_size,
|
|
(pack_func)sec_get_pk_msg__pack);
|
|
|
|
sec_get_pk_msg__free_unpacked(pkm, &pa);
|
|
|
|
if (ret < 0) {
|
|
seclog(sec, LOG_INFO, "error sending reply: %s",
|
|
gnutls_strerror(ret));
|
|
return -1;
|
|
}
|
|
|
|
return ret;
|
|
|
|
case CMD_SEC_SIGN_DATA:
|
|
case CMD_SEC_SIGN_HASH:
|
|
op = sec_op_msg__unpack(&pa, data.size, data.data);
|
|
if (op == NULL) {
|
|
seclog(sec, LOG_INFO, "error unpacking sec op\n");
|
|
return -1;
|
|
}
|
|
|
|
vhost = find_vhost(sec->vconfig, op->vhost);
|
|
assert(vhost != NULL);
|
|
|
|
i = op->key_idx;
|
|
if (op->has_key_idx == 0 || i >= vhost->key_size) {
|
|
seclog(sec, LOG_INFO,
|
|
"%sreceived out-of-bounds key index (%d); have %d keys",
|
|
PREFIX_VHOST(vhost), i, vhost->key_size);
|
|
return -1;
|
|
}
|
|
|
|
data.data = op->data.data;
|
|
data.size = op->data.len;
|
|
|
|
if (cmd == CMD_SEC_SIGN_DATA) {
|
|
ret = gnutls_privkey_sign_data2(vhost->key[i], op->sig,
|
|
0, &data, &out);
|
|
} else {
|
|
ret = gnutls_privkey_sign_hash2(vhost->key[i], op->sig,
|
|
0, &data, &out);
|
|
}
|
|
sec_op_msg__free_unpacked(op, &pa);
|
|
|
|
if (ret < 0) {
|
|
seclog(sec, LOG_INFO, "error in crypto operation: %s",
|
|
gnutls_strerror(ret));
|
|
return -1;
|
|
}
|
|
|
|
ret = handle_op(pool, cfd, sec, cmd, out.data, out.size);
|
|
gnutls_free(out.data);
|
|
|
|
return ret;
|
|
#endif
|
|
case CMD_SEC_SIGN:
|
|
case CMD_SEC_DECRYPT:
|
|
op = sec_op_msg__unpack(&pa, data.size, data.data);
|
|
if (op == NULL) {
|
|
seclog(sec, LOG_INFO, "error unpacking sec op\n");
|
|
return -1;
|
|
}
|
|
|
|
vhost = find_vhost(sec->vconfig, op->vhost);
|
|
assert(vhost != NULL);
|
|
|
|
i = op->key_idx;
|
|
if (op->has_key_idx == 0 || i >= vhost->key_size) {
|
|
seclog(sec, LOG_INFO,
|
|
"%sreceived out-of-bounds key index (%d); have %d keys",
|
|
PREFIX_VHOST(vhost), i, vhost->key_size);
|
|
return -1;
|
|
}
|
|
|
|
data.data = op->data.data;
|
|
data.size = op->data.len;
|
|
|
|
if (cmd == CMD_SEC_DECRYPT) {
|
|
ret = gnutls_privkey_decrypt_data(vhost->key[i], 0,
|
|
&data, &out);
|
|
} else {
|
|
ret = gnutls_privkey_sign_hash(
|
|
vhost->key[i], 0,
|
|
GNUTLS_PRIVKEY_SIGN_FLAG_TLS1_RSA, &data, &out);
|
|
}
|
|
sec_op_msg__free_unpacked(op, &pa);
|
|
|
|
if (ret < 0) {
|
|
seclog(sec, LOG_INFO, "error in crypto operation: %s",
|
|
gnutls_strerror(ret));
|
|
return -1;
|
|
}
|
|
|
|
ret = handle_op(pool, cfd, sec, cmd, out.data, out.size);
|
|
gnutls_free(out.data);
|
|
|
|
return ret;
|
|
|
|
case CMD_SEC_CLI_STATS: {
|
|
CliStatsMsg *tmsg;
|
|
|
|
tmsg = cli_stats_msg__unpack(&pa, data.size, data.data);
|
|
if (tmsg == NULL) {
|
|
seclog(sec, LOG_ERR, "error unpacking data");
|
|
return -1;
|
|
}
|
|
|
|
ret = handle_sec_auth_stats_cmd(sec, tmsg, pid);
|
|
cli_stats_msg__free_unpacked(tmsg, &pa);
|
|
return ret;
|
|
} break;
|
|
|
|
case CMD_SEC_AUTH_INIT: {
|
|
SecAuthInitMsg *auth_init;
|
|
|
|
auth_init =
|
|
sec_auth_init_msg__unpack(&pa, data.size, data.data);
|
|
if (auth_init == NULL) {
|
|
seclog(sec, LOG_INFO, "error unpacking auth init\n");
|
|
return -1;
|
|
}
|
|
|
|
ret = handle_sec_auth_init(cfd, sec, auth_init, pid);
|
|
sec_auth_init_msg__free_unpacked(auth_init, &pa);
|
|
return ret;
|
|
}
|
|
case CMD_SEC_AUTH_CONT: {
|
|
SecAuthContMsg *auth_cont;
|
|
|
|
auth_cont =
|
|
sec_auth_cont_msg__unpack(&pa, data.size, data.data);
|
|
if (auth_cont == NULL) {
|
|
seclog(sec, LOG_INFO, "error unpacking auth cont\n");
|
|
return -1;
|
|
}
|
|
|
|
ret = handle_sec_auth_cont(cfd, sec, auth_cont);
|
|
sec_auth_cont_msg__free_unpacked(auth_cont, &pa);
|
|
return ret;
|
|
}
|
|
case RESUME_STORE_REQ: {
|
|
SessionResumeStoreReqMsg *smsg;
|
|
|
|
smsg = session_resume_store_req_msg__unpack(&pa, buffer_size,
|
|
buffer);
|
|
if (smsg == NULL) {
|
|
seclog(sec, LOG_ERR, "error unpacking data");
|
|
return ERR_BAD_COMMAND;
|
|
}
|
|
|
|
ret = handle_resume_store_req(sec, smsg);
|
|
|
|
/* zeroize the data */
|
|
safe_memset(buffer, 0, buffer_size);
|
|
safe_memset(smsg->session_data.data, 0, smsg->session_data.len);
|
|
|
|
session_resume_store_req_msg__free_unpacked(smsg, &pa);
|
|
|
|
if (ret < 0) {
|
|
seclog(sec, LOG_DEBUG,
|
|
"could not store resumption data");
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case RESUME_DELETE_REQ: {
|
|
SessionResumeFetchMsg *fmsg;
|
|
|
|
fmsg = session_resume_fetch_msg__unpack(&pa, buffer_size,
|
|
buffer);
|
|
if (fmsg == NULL) {
|
|
seclog(sec, LOG_ERR, "error unpacking data");
|
|
return ERR_BAD_COMMAND;
|
|
}
|
|
|
|
ret = handle_resume_delete_req(sec, fmsg);
|
|
|
|
session_resume_fetch_msg__free_unpacked(fmsg, &pa);
|
|
|
|
if (ret < 0) {
|
|
seclog(sec, LOG_DEBUG,
|
|
"could not delete resumption data.");
|
|
}
|
|
}
|
|
|
|
break;
|
|
case RESUME_FETCH_REQ: {
|
|
SessionResumeReplyMsg msg = SESSION_RESUME_REPLY_MSG__INIT;
|
|
SessionResumeFetchMsg *fmsg;
|
|
|
|
fmsg = session_resume_fetch_msg__unpack(&pa, buffer_size,
|
|
buffer);
|
|
if (fmsg == NULL) {
|
|
seclog(sec, LOG_ERR, "error unpacking data");
|
|
return ERR_BAD_COMMAND;
|
|
}
|
|
|
|
ret = handle_resume_fetch_req(sec, fmsg, &msg);
|
|
|
|
session_resume_fetch_msg__free_unpacked(fmsg, &pa);
|
|
|
|
if (ret < 0) {
|
|
msg.reply =
|
|
SESSION_RESUME_REPLY_MSG__RESUME__REP__FAILED;
|
|
seclog(sec, LOG_DEBUG,
|
|
"could not fetch resumption data.");
|
|
} else {
|
|
msg.reply = SESSION_RESUME_REPLY_MSG__RESUME__REP__OK;
|
|
}
|
|
|
|
ret = send_msg(
|
|
pool, cfd, RESUME_FETCH_REP, &msg,
|
|
(pack_size_func)
|
|
session_resume_reply_msg__get_packed_size,
|
|
(pack_func)session_resume_reply_msg__pack);
|
|
|
|
if (ret < 0) {
|
|
seclog(sec, LOG_ERR, "could not send reply cmd %d.",
|
|
(unsigned int)cmd);
|
|
return ERR_BAD_COMMAND;
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
seclog(sec, LOG_WARNING, "unknown type 0x%.2x", cmd);
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int process_packet_from_main(void *pool, int fd, sec_mod_st *sec,
|
|
cmd_request_t cmd, uint8_t *buffer,
|
|
size_t buffer_size)
|
|
{
|
|
gnutls_datum_t data;
|
|
int ret;
|
|
|
|
PROTOBUF_ALLOCATOR(pa, pool);
|
|
|
|
seclog(sec, LOG_DEBUG, "cmd [size=%d] %s\n", (int)buffer_size,
|
|
cmd_request_to_str(cmd));
|
|
data.data = buffer;
|
|
data.size = buffer_size;
|
|
|
|
switch (cmd) {
|
|
case CMD_SECM_RELOAD:
|
|
reload_server(sec);
|
|
|
|
ret = send_msg(pool, fd, CMD_SECM_RELOAD_REPLY, NULL, NULL,
|
|
NULL);
|
|
if (ret < 0) {
|
|
seclog(sec, LOG_ERR,
|
|
"could not send reload reply to main!\n");
|
|
return ERR_BAD_COMMAND;
|
|
}
|
|
break;
|
|
case CMD_SECM_LIST_COOKIES:
|
|
handle_secm_list_cookies_reply(pool, fd, sec);
|
|
|
|
return 0;
|
|
case CMD_SECM_BAN_IP_REPLY: {
|
|
BanIpReplyMsg *msg = NULL;
|
|
|
|
msg = ban_ip_reply_msg__unpack(&pa, data.size, data.data);
|
|
if (msg == NULL) {
|
|
seclog(sec, LOG_INFO,
|
|
"error unpacking auth ban ip reply\n");
|
|
return ERR_BAD_COMMAND;
|
|
}
|
|
|
|
handle_sec_auth_ban_ip_reply(sec, msg);
|
|
ban_ip_reply_msg__free_unpacked(msg, &pa);
|
|
|
|
return 0;
|
|
}
|
|
case CMD_SECM_SESSION_OPEN: {
|
|
SecmSessionOpenMsg *msg;
|
|
|
|
msg = secm_session_open_msg__unpack(&pa, data.size, data.data);
|
|
if (msg == NULL) {
|
|
seclog(sec, LOG_INFO, "error unpacking session open\n");
|
|
return ERR_BAD_COMMAND;
|
|
}
|
|
|
|
ret = handle_secm_session_open_cmd(sec, fd, msg);
|
|
secm_session_open_msg__free_unpacked(msg, &pa);
|
|
|
|
return ret;
|
|
}
|
|
case CMD_SECM_SESSION_CLOSE: {
|
|
SecmSessionCloseMsg *msg;
|
|
|
|
msg = secm_session_close_msg__unpack(&pa, data.size, data.data);
|
|
if (msg == NULL) {
|
|
seclog(sec, LOG_INFO,
|
|
"error unpacking session close\n");
|
|
return ERR_BAD_COMMAND;
|
|
}
|
|
|
|
ret = handle_secm_session_close_cmd(sec, fd, msg);
|
|
secm_session_close_msg__free_unpacked(msg, &pa);
|
|
|
|
return ret;
|
|
}
|
|
case CMD_SECM_TERMINATE_USER_SESSIONS: {
|
|
SecmTerminateUserSessionsMsg *msg;
|
|
SecmTerminateSessionReplyMsg reply =
|
|
SECM_TERMINATE_SESSION_REPLY_MSG__INIT;
|
|
|
|
msg = secm_terminate_user_sessions_msg__unpack(&pa, data.size,
|
|
data.data);
|
|
if (msg == NULL) {
|
|
seclog(sec, LOG_INFO,
|
|
"error unpacking terminate user sessions");
|
|
return ERR_BAD_COMMAND;
|
|
}
|
|
|
|
if (msg->username != NULL)
|
|
reply.result =
|
|
terminate_user_sessions(sec, msg->username);
|
|
|
|
ret = send_msg(
|
|
pool, fd, CMD_SECM_TERMINATE_USER_SESSIONS_REPLY,
|
|
&reply,
|
|
(pack_size_func)
|
|
secm_terminate_session_reply_msg__get_packed_size,
|
|
(pack_func)secm_terminate_session_reply_msg__pack);
|
|
|
|
secm_terminate_user_sessions_msg__free_unpacked(msg, &pa);
|
|
|
|
if (ret < 0) {
|
|
seclog(sec, LOG_ERR,
|
|
"could not send terminate session reply!");
|
|
return ERR_BAD_COMMAND;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
case CMD_SECM_TERMINATE_SESSION: {
|
|
SecmTerminateSessionMsg *msg;
|
|
SecmTerminateSessionReplyMsg reply =
|
|
SECM_TERMINATE_SESSION_REPLY_MSG__INIT;
|
|
|
|
msg = secm_terminate_session_msg__unpack(&pa, data.size,
|
|
data.data);
|
|
if (msg == NULL) {
|
|
seclog(sec, LOG_INFO,
|
|
"error unpacking terminate session");
|
|
return ERR_BAD_COMMAND;
|
|
}
|
|
|
|
if (msg->safe_id.data != NULL && msg->safe_id.len > 0)
|
|
reply.result = terminate_session_by_sid(
|
|
sec, (const char *)msg->safe_id.data,
|
|
msg->safe_id.len);
|
|
|
|
ret = send_msg(
|
|
pool, fd, CMD_SECM_TERMINATE_SESSION_REPLY, &reply,
|
|
(pack_size_func)
|
|
secm_terminate_session_reply_msg__get_packed_size,
|
|
(pack_func)secm_terminate_session_reply_msg__pack);
|
|
|
|
secm_terminate_session_msg__free_unpacked(msg, &pa);
|
|
|
|
if (ret < 0) {
|
|
seclog(sec, LOG_ERR,
|
|
"could not send terminate session reply!");
|
|
return ERR_BAD_COMMAND;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
default:
|
|
seclog(sec, LOG_WARNING, "unknown type 0x%.2x", cmd);
|
|
return ERR_BAD_COMMAND;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void handle_alarm(int signo)
|
|
{
|
|
need_maintenance = 1;
|
|
}
|
|
|
|
static void handle_sigterm(int signo)
|
|
{
|
|
need_exit = 1;
|
|
}
|
|
|
|
static void send_stats_to_main(sec_mod_st *sec)
|
|
{
|
|
int ret;
|
|
time_t now = time(NULL);
|
|
SecmStatsMsg msg = SECM_STATS_MSG__INIT;
|
|
|
|
if (GETPCONFIG(sec)->stats_reset_time != 0 &&
|
|
now - sec->last_stats_reset > GETPCONFIG(sec)->stats_reset_time) {
|
|
sec->auth_failures = 0;
|
|
sec->avg_auth_time = 0;
|
|
sec->max_auth_time = 0;
|
|
sec->last_stats_reset = now;
|
|
}
|
|
|
|
msg.secmod_client_entries = sec_mod_client_db_elems(sec);
|
|
msg.secmod_tlsdb_entries = sec->tls_db.entries;
|
|
msg.secmod_auth_failures = sec->auth_failures;
|
|
msg.secmod_avg_auth_time = sec->avg_auth_time;
|
|
msg.secmod_max_auth_time = sec->max_auth_time;
|
|
/* we only report the number of failures since last call */
|
|
sec->auth_failures = 0;
|
|
|
|
/* the following two are not resettable */
|
|
msg.secmod_client_entries = sec_mod_client_db_elems(sec);
|
|
msg.secmod_tlsdb_entries = sec->tls_db.entries;
|
|
|
|
ret = send_msg(sec, sec->cmd_fd, CMD_SECM_STATS, &msg,
|
|
(pack_size_func)secm_stats_msg__get_packed_size,
|
|
(pack_func)secm_stats_msg__pack);
|
|
if (ret < 0) {
|
|
seclog(sec, LOG_ERR, "error in sending statistics to main");
|
|
return;
|
|
}
|
|
}
|
|
|
|
static void reload_server(sec_mod_st *sec)
|
|
{
|
|
vhost_cfg_st *vhost = NULL;
|
|
|
|
seclog(sec, LOG_DEBUG, "reloading configuration");
|
|
reload_cfg_file(sec, sec->vconfig, 1);
|
|
load_keys(sec, 0);
|
|
|
|
list_for_each(sec->vconfig, vhost, list)
|
|
{
|
|
sec_auth_init(vhost);
|
|
}
|
|
sup_config_init(sec);
|
|
|
|
need_reload = 0;
|
|
}
|
|
|
|
static void check_other_work(sec_mod_st *sec)
|
|
{
|
|
vhost_cfg_st *vhost = NULL;
|
|
|
|
if (need_exit) {
|
|
unsigned int i;
|
|
|
|
list_for_each(sec->vconfig, vhost, list)
|
|
{
|
|
for (i = 0; i < vhost->key_size; i++) {
|
|
gnutls_privkey_deinit(vhost->key[i]);
|
|
vhost->key[i] = NULL;
|
|
}
|
|
vhost->key_size = 0;
|
|
}
|
|
|
|
sec_mod_client_db_deinit(sec);
|
|
tls_cache_deinit(&sec->tls_db);
|
|
talloc_free(sec->config_pool);
|
|
talloc_free(sec->sec_mod_pool);
|
|
exit(EXIT_SUCCESS);
|
|
}
|
|
|
|
if (need_reload) {
|
|
reload_server(sec);
|
|
}
|
|
|
|
if (need_maintenance) {
|
|
seclog(sec, LOG_DEBUG, "performing maintenance");
|
|
cleanup_client_entries(sec);
|
|
expire_tls_sessions(sec);
|
|
send_stats_to_main(sec);
|
|
seclog(sec, LOG_DEBUG, "active sessions %d",
|
|
sec_mod_client_db_elems(sec));
|
|
alarm(MAINTENANCE_TIME);
|
|
need_maintenance = 0;
|
|
}
|
|
}
|
|
|
|
static int serve_request_main(sec_mod_st *sec, int fd, uint8_t *buffer,
|
|
unsigned int buffer_size)
|
|
{
|
|
int ret, e;
|
|
uint8_t cmd;
|
|
size_t length;
|
|
void *pool = buffer;
|
|
|
|
/* read request */
|
|
ret = recv_msg_headers(fd, &cmd, MAIN_SEC_MOD_TIMEOUT);
|
|
if (ret < 0) {
|
|
seclog(sec, LOG_ERR, "error receiving msg head from main");
|
|
ret = ERR_BAD_COMMAND;
|
|
goto leave;
|
|
}
|
|
|
|
length = ret;
|
|
|
|
seclog(sec, LOG_DEBUG, "received request %s", cmd_request_to_str(cmd));
|
|
if (cmd <= MIN_SECM_CMD || cmd >= MAX_SECM_CMD) {
|
|
seclog(sec, LOG_ERR,
|
|
"received invalid message from main of %u bytes (cmd: %u)\n",
|
|
(unsigned int)length, (unsigned int)cmd);
|
|
return ERR_BAD_COMMAND;
|
|
}
|
|
|
|
if (length > buffer_size) {
|
|
seclog(sec, LOG_ERR, "received too big message (%d)",
|
|
(int)length);
|
|
ret = ERR_BAD_COMMAND;
|
|
goto leave;
|
|
}
|
|
|
|
/* read the body */
|
|
ret = force_read_timeout(fd, buffer, length, MAIN_SEC_MOD_TIMEOUT);
|
|
if (ret < 0) {
|
|
e = errno;
|
|
seclog(sec, LOG_ERR,
|
|
"error receiving msg body of cmd %u with length %u: %s",
|
|
cmd, (unsigned int)length, strerror(e));
|
|
ret = ERR_BAD_COMMAND;
|
|
goto leave;
|
|
}
|
|
|
|
ret = process_packet_from_main(pool, fd, sec, cmd, buffer, ret);
|
|
if (ret < 0) {
|
|
seclog(sec, LOG_ERR,
|
|
"error processing data for '%s' command (%d)",
|
|
cmd_request_to_str(cmd), ret);
|
|
}
|
|
|
|
leave:
|
|
return ret;
|
|
}
|
|
|
|
static int serve_request_worker(sec_mod_st *sec, int cfd, pid_t pid,
|
|
uint8_t *buffer, unsigned int buffer_size)
|
|
{
|
|
int ret, e;
|
|
uint8_t cmd;
|
|
size_t length;
|
|
void *pool = buffer;
|
|
|
|
/* read request */
|
|
ret = recv_msg_headers(cfd, &cmd, MAX_WAIT_SECS);
|
|
if (ret < 0) {
|
|
seclog(sec, LOG_DEBUG, "error receiving msg head from worker");
|
|
goto leave;
|
|
}
|
|
|
|
length = ret;
|
|
|
|
if (length > buffer_size) {
|
|
seclog(sec, LOG_INFO, "too big message (%d)", (int)length);
|
|
ret = -1;
|
|
goto leave;
|
|
}
|
|
|
|
/* read the body */
|
|
ret = force_read_timeout(cfd, buffer, length, MAX_WAIT_SECS);
|
|
if (ret < 0) {
|
|
e = errno;
|
|
seclog(sec, LOG_INFO, "error receiving msg body: %s",
|
|
strerror(e));
|
|
ret = -1;
|
|
goto leave;
|
|
}
|
|
|
|
ret = process_worker_packet(pool, cfd, pid, sec, cmd, buffer, ret);
|
|
if (ret < 0) {
|
|
seclog(sec, LOG_DEBUG, "error processing '%s' command (%d)",
|
|
cmd_request_to_str(cmd), ret);
|
|
}
|
|
|
|
leave:
|
|
return ret;
|
|
}
|
|
|
|
#define CHECK_LOOP_ERR(x) \
|
|
{ \
|
|
if (force != 0) { \
|
|
GNUTLS_FATAL_ERR(x); \
|
|
} else { \
|
|
if (ret < 0) { \
|
|
seclog(sec, LOG_ERR, \
|
|
"could not reload key %s", \
|
|
vhost->perm_config.key[i]); \
|
|
continue; \
|
|
} \
|
|
} \
|
|
}
|
|
|
|
static void read_private_key(sec_mod_st *sec, vhost_cfg_st *vhost,
|
|
unsigned int force)
|
|
{
|
|
int ret;
|
|
unsigned int i;
|
|
|
|
/* read private keys */
|
|
for (i = 0; i < vhost->key_size; i++) {
|
|
gnutls_privkey_t p;
|
|
|
|
ret = gnutls_privkey_init(&p);
|
|
CHECK_LOOP_ERR(ret);
|
|
|
|
/* load the private key */
|
|
if (gnutls_url_is_supported(vhost->perm_config.key[i]) != 0) {
|
|
gnutls_privkey_set_pin_function(p, pin_callback,
|
|
&vhost->pins);
|
|
ret = gnutls_privkey_import_url(
|
|
p, vhost->perm_config.key[i], 0);
|
|
CHECK_LOOP_ERR(ret);
|
|
} else {
|
|
gnutls_datum_t data;
|
|
|
|
ret = gnutls_load_file(vhost->perm_config.key[i],
|
|
&data);
|
|
if (ret < 0) {
|
|
seclog(sec, LOG_ERR, "error loading file '%s'",
|
|
vhost->perm_config.key[i]);
|
|
CHECK_LOOP_ERR(ret);
|
|
}
|
|
|
|
ret = gnutls_privkey_import_x509_raw(
|
|
p, &data, GNUTLS_X509_FMT_PEM, NULL, 0);
|
|
/* GnuTLS 3.7.3 introduces a backwards incompatible change and
|
|
* GNUTLS_E_PKCS11_PIN_ERROR is returned when an encrypted
|
|
* file is loaded https://gitlab.com/gnutls/gnutls/-/issues/1321 */
|
|
if ((ret == GNUTLS_E_DECRYPTION_FAILED ||
|
|
ret == GNUTLS_E_PKCS11_PIN_ERROR) &&
|
|
vhost->pins.pin[0]) {
|
|
ret = gnutls_privkey_import_x509_raw(
|
|
p, &data, GNUTLS_X509_FMT_PEM,
|
|
vhost->pins.pin, 0);
|
|
}
|
|
CHECK_LOOP_ERR(ret);
|
|
gnutls_free(data.data);
|
|
}
|
|
|
|
if (vhost->key[i] != NULL) {
|
|
gnutls_privkey_deinit(vhost->key[i]);
|
|
}
|
|
vhost->key[i] = p;
|
|
}
|
|
seclog(sec, LOG_DEBUG, "%sloaded %d keys\n", PREFIX_VHOST(vhost),
|
|
vhost->key_size);
|
|
}
|
|
|
|
static int load_keys(sec_mod_st *sec, unsigned int force)
|
|
{
|
|
unsigned int i, reload_file;
|
|
int ret;
|
|
vhost_cfg_st *vhost = NULL;
|
|
|
|
list_for_each_rev(sec->vconfig, vhost, list)
|
|
{
|
|
if (force == 0) {
|
|
reload_file = 0;
|
|
|
|
for (i = 0; i < vhost->perm_config.key_size; i++) {
|
|
if (need_file_reload(vhost->perm_config.key[i],
|
|
vhost->cert_last_access) !=
|
|
0) {
|
|
reload_file = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (reload_file == 0)
|
|
continue;
|
|
}
|
|
|
|
vhost->cert_last_access = time(NULL);
|
|
|
|
ret = load_pins(GETPCONFIG(sec), &vhost->pins);
|
|
if (ret < 0) {
|
|
seclog(sec, LOG_ERR, "error loading PIN files");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
/* Reminder: the number of private keys or their filenames cannot be changed on reload
|
|
*/
|
|
if (vhost->key == NULL) {
|
|
vhost->key_size = vhost->perm_config.key_size;
|
|
vhost->key = talloc_zero_size(
|
|
sec, sizeof(*vhost->key) *
|
|
vhost->perm_config.key_size);
|
|
if (vhost->key == NULL) {
|
|
seclog(sec, LOG_ERR,
|
|
"error in memory allocation");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
|
|
read_private_key(sec, vhost, force);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* sec_mod_server:
|
|
* @config: server configuration
|
|
* @socket_file: the name of the socket
|
|
* @cmd_fd: socket to exchange commands with main
|
|
* @cmd_fd_sync: socket to received sync commands from main
|
|
*
|
|
* This is the main part of the security module.
|
|
* It creates the unix domain socket identified by @socket_file
|
|
* and then accepts connections from the workers to it. Then
|
|
* it serves commands requested on the server's private key.
|
|
*
|
|
* When the operation is decrypt the provided data are
|
|
* decrypted and sent back to worker. The sign operation
|
|
* signs the provided data.
|
|
*
|
|
* The security module's reply to the worker has the
|
|
* following format:
|
|
* byte[0-5]: length (uint32_t)
|
|
* byte[5-total]: data (signature or decrypted data)
|
|
*
|
|
* The reason for having this as a separate process
|
|
* is to avoid any bug on the workers to leak the key.
|
|
* It is not part of main because workers are spawned
|
|
* from main, and thus should be prevented from accessing
|
|
* parts the key in stack or heap that was not zeroized.
|
|
* Other than that it allows the main server to spawn
|
|
* clients fast without becoming a bottleneck due to private
|
|
* key operations.
|
|
*/
|
|
void sec_mod_server(void *main_pool, void *config_pool,
|
|
struct list_head *vconfig, const char *socket_file,
|
|
int cmd_fd, int cmd_fd_sync, size_t hmac_key_length,
|
|
const uint8_t *hmac_key, const uint8_t instance_id)
|
|
{
|
|
struct sockaddr_un sa;
|
|
socklen_t sa_len;
|
|
int cfd, ret, e, n;
|
|
unsigned int buffer_size;
|
|
uid_t uid;
|
|
uint8_t *buffer;
|
|
int sd;
|
|
sec_mod_st *sec = NULL;
|
|
void *sec_mod_pool;
|
|
vhost_cfg_st *vhost = NULL;
|
|
fd_set rd_set;
|
|
pid_t pid;
|
|
#ifdef HAVE_PSELECT
|
|
struct timespec ts;
|
|
#else
|
|
struct timeval ts;
|
|
#endif
|
|
sigset_t emptyset, blockset;
|
|
|
|
#ifdef DEBUG_LEAKS
|
|
talloc_enable_leak_report_full();
|
|
#endif
|
|
sigemptyset(&blockset);
|
|
sigemptyset(&emptyset);
|
|
sigaddset(&blockset, SIGALRM);
|
|
sigaddset(&blockset, SIGTERM);
|
|
sigaddset(&blockset, SIGINT);
|
|
sigaddset(&blockset, SIGHUP);
|
|
|
|
sec_mod_pool = talloc_init("sec-mod");
|
|
if (sec_mod_pool == NULL) {
|
|
seclog(sec, LOG_ERR, "error in memory allocation");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
sec = talloc_zero(sec_mod_pool, sec_mod_st);
|
|
if (sec == NULL) {
|
|
seclog(sec, LOG_ERR, "error in memory allocation");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
sec->vconfig = vconfig;
|
|
sec->config_pool = config_pool;
|
|
sec->sec_mod_pool = sec_mod_pool;
|
|
memcpy((uint8_t *)sec->hmac_key, hmac_key, hmac_key_length);
|
|
sec->sec_mod_instance_id = instance_id;
|
|
|
|
tls_cache_init(sec, &sec->tls_db);
|
|
sup_config_init(sec);
|
|
|
|
memset(&sa, 0, sizeof(sa));
|
|
sa.sun_family = AF_UNIX;
|
|
strlcpy(sa.sun_path, socket_file, sizeof(sa.sun_path));
|
|
(void)remove(socket_file);
|
|
|
|
#define SOCKET_FILE sa.sun_path
|
|
|
|
/* we no longer need the main pool after this point. */
|
|
talloc_free(main_pool);
|
|
|
|
ocsignal(SIGHUP, SIG_IGN);
|
|
ocsignal(SIGINT, handle_sigterm);
|
|
ocsignal(SIGTERM, handle_sigterm);
|
|
ocsignal(SIGALRM, handle_alarm);
|
|
|
|
list_for_each(sec->vconfig, vhost, list)
|
|
{
|
|
sec_auth_init(vhost);
|
|
}
|
|
|
|
sec->cmd_fd = cmd_fd;
|
|
sec->cmd_fd_sync = cmd_fd_sync;
|
|
|
|
if (sec_mod_client_db_init(sec) == NULL) {
|
|
seclog(sec, LOG_ERR, "error in client db initialization");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
sd = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
if (sd == -1) {
|
|
e = errno;
|
|
seclog(sec, LOG_ERR, "could not create socket '%s': %s",
|
|
SOCKET_FILE, strerror(e));
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
set_cloexec_flag(sd, 1);
|
|
|
|
umask(066);
|
|
ret = bind(sd, (struct sockaddr *)&sa, SUN_LEN(&sa));
|
|
if (ret == -1) {
|
|
e = errno;
|
|
seclog(sec, LOG_ERR, "could not bind socket '%s': %s",
|
|
SOCKET_FILE, strerror(e));
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
ret = chown(SOCKET_FILE, GETPCONFIG(sec)->uid, GETPCONFIG(sec)->gid);
|
|
if (ret == -1) {
|
|
e = errno;
|
|
seclog(sec, LOG_INFO, "could not chown socket '%s': %s",
|
|
SOCKET_FILE, strerror(e));
|
|
}
|
|
|
|
ret = listen(sd, 1024);
|
|
if (ret == -1) {
|
|
e = errno;
|
|
seclog(sec, LOG_ERR, "could not listen to socket '%s': %s",
|
|
SOCKET_FILE, strerror(e));
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
ret = load_keys(sec, 1);
|
|
if (ret < 0) {
|
|
seclog(sec, LOG_ERR, "error loading private key files");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
sigprocmask(SIG_BLOCK, &blockset, &sig_default_set);
|
|
alarm(MAINTENANCE_TIME);
|
|
seclog(sec, LOG_INFO, "sec-mod initialized (socket: %s)", SOCKET_FILE);
|
|
|
|
for (;;) {
|
|
check_other_work(sec);
|
|
|
|
FD_ZERO(&rd_set);
|
|
n = 0;
|
|
|
|
FD_SET(cmd_fd, &rd_set);
|
|
n = MAX(n, cmd_fd);
|
|
|
|
FD_SET(cmd_fd_sync, &rd_set);
|
|
n = MAX(n, cmd_fd_sync);
|
|
|
|
FD_SET(sd, &rd_set);
|
|
n = MAX(n, sd);
|
|
|
|
#ifdef HAVE_PSELECT
|
|
ts.tv_nsec = 0;
|
|
ts.tv_sec = 120;
|
|
ret = pselect(n + 1, &rd_set, NULL, NULL, &ts, &emptyset);
|
|
#else
|
|
ts.tv_usec = 0;
|
|
ts.tv_sec = 120;
|
|
sigprocmask(SIG_UNBLOCK, &blockset, NULL);
|
|
ret = select(n + 1, &rd_set, NULL, NULL, &ts);
|
|
sigprocmask(SIG_BLOCK, &blockset, NULL);
|
|
#endif
|
|
if (ret == 0 || (ret == -1 && errno == EINTR))
|
|
continue;
|
|
|
|
if (ret < 0) {
|
|
e = errno;
|
|
seclog(sec, LOG_ERR, "Error in pselect(): %s",
|
|
strerror(e));
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
/* we do a new allocation, to also use it as pool for the
|
|
* parsers to use */
|
|
buffer_size = MAX_MSG_SIZE;
|
|
buffer = talloc_size(sec, buffer_size);
|
|
if (buffer == NULL) {
|
|
seclog(sec, LOG_ERR, "error in memory allocation");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
/* we use two fds for communication with main. The synchronous is for
|
|
* ping-pong communication where each request is answered immediately. The
|
|
* async is for messages sent back and forth in no particular order */
|
|
if (FD_ISSET(cmd_fd_sync, &rd_set)) {
|
|
ret = serve_request_main(sec, cmd_fd_sync, buffer,
|
|
buffer_size);
|
|
if (ret < 0 && ret == ERR_BAD_COMMAND) {
|
|
seclog(sec, LOG_ERR,
|
|
"error processing sync command from main");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
|
|
if (FD_ISSET(cmd_fd, &rd_set)) {
|
|
ret = serve_request_main(sec, cmd_fd, buffer,
|
|
buffer_size);
|
|
if (ret < 0 && ret == ERR_BAD_COMMAND) {
|
|
seclog(sec, LOG_ERR,
|
|
"error processing async command from main");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
|
|
if (FD_ISSET(sd, &rd_set)) {
|
|
sa_len = sizeof(sa);
|
|
cfd = accept(sd, (struct sockaddr *)&sa, &sa_len);
|
|
if (cfd == -1) {
|
|
e = errno;
|
|
if (e != EINTR) {
|
|
seclog(sec, LOG_DEBUG,
|
|
"sec-mod error accepting connection: %s",
|
|
strerror(e));
|
|
goto cont;
|
|
}
|
|
}
|
|
set_cloexec_flag(cfd, 1);
|
|
|
|
/* do not allow unauthorized processes to issue commands
|
|
*/
|
|
ret = check_upeer_id("sec-mod",
|
|
GETPCONFIG(sec)->log_level, cfd,
|
|
GETPCONFIG(sec)->uid,
|
|
GETPCONFIG(sec)->gid, &uid, &pid);
|
|
if (ret < 0) {
|
|
seclog(sec, LOG_INFO,
|
|
"rejected unauthorized connection");
|
|
} else {
|
|
memset(buffer, 0, buffer_size);
|
|
serve_request_worker(sec, cfd, pid, buffer,
|
|
buffer_size);
|
|
}
|
|
close(cfd);
|
|
}
|
|
cont:
|
|
talloc_free(buffer);
|
|
#ifdef DEBUG_LEAKS
|
|
talloc_report_full(sec, stderr);
|
|
#endif
|
|
}
|
|
}
|