* Copyright (C) 2019 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "config.h"
#include "client-certificate.h"
#include <assert.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <regex.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_PIDFD_GETPID
#include <sys/pidfd.h>
#endif
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <syslog.h>
#include <unistd.h>
#include <systemd/sd-bus.h>
#include "session-utils.h"
#define CLIENT_CERTIFICATE_DIRECTORY "/run/cockpit/tls/clients"
static int
open_proc_pid (pid_t pid)
{
char path[100];
int r = snprintf (path, sizeof path, "/proc/%lu", (unsigned long) pid);
if (r < 0 || r >= sizeof path)
errx (EX, "memory error");
int fd = open (path, O_DIRECTORY | O_NOFOLLOW | O_PATH | O_CLOEXEC);
if (fd < 0)
err (EX, "failed to open %s", path);
return fd;
}
static size_t
read_proc_file (int dirfd, const char *name, char *buffer, size_t bufsize)
{
int fd = openat (dirfd, name, O_RDONLY | O_CLOEXEC);
if (fd < 0)
err (EX, "Failed to open %s proc file", name);
* signal handlers which survive the login */
ssize_t len = read (fd, buffer, bufsize);
if (len < 0)
err (EX, "Failed to read /proc file %s", name);
close (fd);
if (len >= bufsize)
errx (EX, "proc file %s exceeds buffer size %zu", name, bufsize);
buffer[len] = '\0';
return len;
}
* desirable: Let's not get DoSed by huge certs */
#define MAX_PEER_CERT_SIZE 100000
* including "0::" prefix and newline.
* NB: the kernel doesn't allow newlines in cgroup names. */
static char *
read_proc_pid_cgroup (int dirfd, size_t *out_length)
{
char buffer[1024];
size_t len = read_proc_file (dirfd, "cgroup", buffer, sizeof buffer);
if (strncmp (buffer, "0::/", 4) == 0 &&
buffer[len - 1] == '\n')
{
*out_length = len;
return strdupx (buffer);
}
warnx ("unexpected cgroups content, certificate matching only supports cgroup v2: '%s'", buffer);
exit_init_problem ("authentication-unavailable", "certificate matching only supports cgroup v2");
}
static
unsigned long long get_proc_pid_start_time (int dirfd)
{
char buffer[4096];
read_proc_file (dirfd, "stat", buffer, sizeof buffer);
* field can contain the ')' character, search backwards for this to avoid malicious
* processes trying to fool us; See proc_pid_stat(5) */
const char *p = strrchr (buffer, ')');
if (p == NULL)
errx (EX, "Failed to find process name in /proc/pid/stat: %s", buffer);
for (int i = 0; i <= 19; i++)
{
p = strchr (p, ' ');
if (p == NULL)
errx (EX, "Failed to find start time in /proc/pid/stat");
++p;
}
char *endptr;
unsigned long long start_time = strtoull (p, &endptr, 10);
if (*endptr != ' ')
errx (EX, "Failed to parse start time in /proc/pid/stat from %s", p);
return start_time;
}
static int
get_ws_proc_fd_pid_time (int unix_fd)
{
struct ucred ucred;
socklen_t ucred_len = sizeof ucred;
if (getsockopt (unix_fd, SOL_SOCKET, SO_PEERCRED, &ucred, &ucred_len) != 0 ||
ucred_len != sizeof ucred)
{
debug ("failed to read stdin peer credentials: %m; not in socket mode?");
warnx ("Certificate authentication only supported with cockpit-session.socket");
exit_init_problem ("authentication-unavailable", "Certificate authentication only supported with cockpit-session.socket");
}
debug ("unix socket mode, ws peer pid %d", ucred.pid);
int ws_proc_dirfd = open_proc_pid (ucred.pid);
unsigned long long ws_start_time = get_proc_pid_start_time (ws_proc_dirfd);
int my_pid_dirfd = open_proc_pid (getpid ());
unsigned long long my_start_time = get_proc_pid_start_time (my_pid_dirfd);
close (my_pid_dirfd);
debug ("peer start time: %llu, my start time: %llu", ws_start_time, my_start_time);
* the original pid, they can trick a different user to login, get the old pid (pointing to their cgroup), and
* capture their session. To prevent that, require that ws must have started earlier than ourselves. */
if (my_start_time < ws_start_time)
{
warnx ("start time of this process (%llu) is older than cockpit-ws (%llu), pid recycling attack?",
my_start_time, ws_start_time);
close (ws_proc_dirfd);
exit_init_problem ("access-denied", "implausible cockpit-ws start time");
}
return ws_proc_dirfd;
}
* We only support being called via cockpit-session.socket (i.e. Unix socket).
*/
static int
get_ws_proc_fd (int unix_fd)
{
#if defined(SO_PEERPIDFD) && defined(HAVE_PIDFD_GETPID)
int pidfd = -1;
socklen_t socklen = sizeof pidfd;
if (getsockopt (unix_fd, SOL_SOCKET, SO_PEERPIDFD, &pidfd, &socklen) < 0)
{
if (errno == ENOPROTOOPT)
{
debug ("SO_PEERPIDFD not supported: %m, falling back to pid/time check");
return get_ws_proc_fd_pid_time (unix_fd);
}
warn ("Failed to get peer pidfd");
exit_init_problem ("access-denied", "Failed to get peer pidfd");
}
if (socklen != sizeof pidfd)
errx (EX, "SO_PEERPIDFD returned too small result");
pid_t pid = pidfd_getpid (pidfd);
if (pid < 0)
{
* support it; but err on the side of denying access rather than falling back */
warn ("Failed to get pid from pidfd");
exit_init_problem ("access-denied", "Failed to get pid from pidfd");
}
debug ("pid from ws peer pidfd: %i", (int) pid);
int ws_proc_dirfd = open_proc_pid (pid);
if (pidfd_getpid (pidfd) != pid)
{
warn ("original pid %i is not valid any more", (int) pid);
exit_init_problem ("access-denied", "Failed to get cockpit-ws pid");
}
close (pidfd);
return ws_proc_dirfd;
#else
debug ("not built with pidfd support, falling back to pid/time check");
return get_ws_proc_fd_pid_time (unix_fd);
#endif
}
* @str: a string
*
* Ensures that str is a hexadecimal character string, exactly 64
* characters in length.
*/
static bool
valid_256_bit_hex_string (const char *str)
{
size_t length = strspn (str, "0123456789abcdef");
return str[length] == '\0' && length == 64;
}
* read_cert_file:
* @contents: a buffer to read the certificate into
* @contents_size: the size of @contents
*
* Reads the contents of the certificate file into @contents (of size @contents_size).
* The buffer must be large enough for the contents of the certificate file, plus
* a nul terminator (which will be added).
*
* On success, the size of the certificate file (excluding nul
* terminator) is returned. This value is never 0. On error, -1 is
* returned with errno not guaranteed to be set (but a message will be
* logged).
*/
static ssize_t
read_cert_file (const char *filename,
char *contents,
size_t contents_size)
{
int dirfd = -1, filefd = -1;
ssize_t result = -1;
struct stat buf;
ssize_t r;
if (!valid_256_bit_hex_string (filename))
{
warnx ("tls-cert authentication token is invalid");
goto out;
}
dirfd = open (CLIENT_CERTIFICATE_DIRECTORY, O_PATH | O_DIRECTORY | O_NOFOLLOW);
if (dirfd == -1)
{
warn ("Failed to open " CLIENT_CERTIFICATE_DIRECTORY);
goto out;
}
filefd = openat (dirfd, filename, O_RDONLY | O_NOFOLLOW);
if (filefd == -1)
{
warn ("Failed to open certificate file %s/%s",
CLIENT_CERTIFICATE_DIRECTORY, filename);
goto out;
}
if (fstat (filefd, &buf) != 0)
{
warn ("Failed to stat certificate file %s/%s",
CLIENT_CERTIFICATE_DIRECTORY, filename);
goto out;
}
if (!S_ISREG (buf.st_mode))
{
warnx ("Could not read certificate: %s/%s is not a regular file",
CLIENT_CERTIFICATE_DIRECTORY, filename);
goto out;
}
if (buf.st_size == 0)
{
warnx ("Could not read certificate: %s/%s is empty",
CLIENT_CERTIFICATE_DIRECTORY, filename);
goto out;
}
if (!(buf.st_size < contents_size))
{
warnx ("Insufficient space in read buffer for %s/%s",
CLIENT_CERTIFICATE_DIRECTORY, filename);
goto out;
}
do
r = pread (filefd, contents, buf.st_size, 0);
while (r == -1 && errno == EINTR);
if (r == -1)
{
warn ("Could not read certificate file %s/%s",
CLIENT_CERTIFICATE_DIRECTORY, filename);
goto out;
}
if (r != buf.st_size)
{
warnx ("Read incomplete contents of certificate file %s/%s: %zu of %zu bytes",
CLIENT_CERTIFICATE_DIRECTORY, filename, r, (size_t) buf.st_size);
goto out;
}
contents[buf.st_size] = '\0';
if (strlen (contents) != buf.st_size)
{
warnx ("Certificate file %s/%s contains nul characters",
CLIENT_CERTIFICATE_DIRECTORY, filename);
goto out;
}
result = buf.st_size;
out:
if (filefd != -1)
close (filefd);
if (dirfd != -1)
close (dirfd);
return result;
}
static bool
sssd_map_certificate (const char *certificate, char** username)
{
int result = false;
sd_bus_error err = SD_BUS_ERROR_NULL;
sd_bus *bus = NULL;
sd_bus_message *user_obj_msg = NULL;
const char *user_obj_path = NULL;
int r;
assert (username);
assert (!*username);
r = sd_bus_open_system (&bus);
if (r < 0)
{
warnx ("Failed to connect to system bus: %s", strerror (-r));
goto out;
}
r = sd_bus_call_method (bus,
"org.freedesktop.sssd.infopipe",
"/org/freedesktop/sssd/infopipe/Users",
"org.freedesktop.sssd.infopipe.Users",
"FindByValidCertificate",
&err,
&user_obj_msg,
"s",
certificate);
if (r < 0)
{
if (sd_bus_error_has_name (&err, "sbus.Error.NotFound"))
{
warnx ("No matching user for certificate");
goto out;
}
warnx ("Failed to map certificate to user: [%s] %s", err.name, err.message);
goto out;
}
assert (user_obj_msg);
r = sd_bus_message_read (user_obj_msg, "o", &user_obj_path);
if (r < 0)
{
warnx ("Failed to parse response message: %s", strerror (-r));
goto out;
}
debug ("certificate mapped to user object path %s", user_obj_path);
r = sd_bus_get_property_string (bus,
"org.freedesktop.sssd.infopipe",
user_obj_path,
"org.freedesktop.sssd.infopipe.Users.User",
"name",
&err,
username);
if (r < 0)
{
warnx ("Failed to map user object to name: [%s] %s", err.name, err.message);
goto out;
}
assert (*username);
debug ("mapped certificate to user %s", *username);
result = true;
out:
sd_bus_error_free (&err);
sd_bus_message_unref (user_obj_msg);
sd_bus_unref (bus);
return result;
}
* cockpit_session_client_certificate_map_user
*
* Read the given certificate file, ensure that it belongs to our own cgroup, and ask
* sssd to map it to a user. If everything matches as expected, return the user name.
* Otherwise exit the process with sending an appropriate error to stdout using the
* Cockpit protocol.
*/
char *
cockpit_session_client_certificate_map_user (const char *client_certificate_filename)
{
char cert_pem[MAX_PEER_CERT_SIZE];
char *sssd_user = NULL;
if (read_cert_file (client_certificate_filename, cert_pem, sizeof cert_pem) < 0)
{
warnx ("No https instance certificate present");
exit_init_problem ("authentication-unavailable", "No https instance certificate present");
}
int ws_proc_dirfd = get_ws_proc_fd (STDIN_FILENO);
size_t ws_cgroup_length;
char *ws_cgroup = read_proc_pid_cgroup (ws_proc_dirfd, &ws_cgroup_length);
assert (ws_cgroup);
close (ws_proc_dirfd);
* subsequent comparison */
if (ws_cgroup[ws_cgroup_length - 1] != '\n')
errx (EX, "cgroup does not end in newline");
* contains exactly one newline (at the end), and the expected
* value of ws_cgroup is on the first line in cert_pem.
*/
if (strncmp (cert_pem, ws_cgroup, ws_cgroup_length) != 0)
{
warnx ("This client certificate is only meant to be used from another cgroup");
free (ws_cgroup);
exit_init_problem ("access-denied", "mismatching client certificate");
}
free (ws_cgroup);
if (!sssd_map_certificate (cert_pem + ws_cgroup_length, &sssd_user))
exit_init_problem ("authentication-failed", "sssd does not know this certificate");
return sssd_user;
}