Files
pam-fprint-grosshack/pam/pam_fprintd.c
Benjamin Berg c24badfd68 pam: Move NameOwnerChanged registration after initialization
We must ignore NameOwnerChanged that happen due to automatic startup.
The easy way to do so is to just register it only when we get to the
point that a name owner change has security implications.

While add it, change it to always log at a warning level.

Fixes: #94
2020-12-11 10:34:51 +01:00

818 lines
22 KiB
C

/*
* pam_fprint: PAM module for fingerprint authentication through fprintd
* Copyright (C) 2007 Daniel Drake <dsd@gentoo.org>
* Copyright (C) 2008-2014, 2017-2020 Bastien Nocera <hadess@hadess.net>
*
* This program 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.
*
* This program 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <config.h>
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <syslog.h>
#include <errno.h>
#include <libintl.h>
#include <systemd/sd-bus.h>
#include <systemd/sd-login.h>
#define PAM_SM_AUTH
#include <security/pam_modules.h>
#include <security/pam_ext.h>
#define _(s) ((char *) dgettext (GETTEXT_PACKAGE, s))
#define TR(s) dgettext (GETTEXT_PACKAGE, s)
#define N_(s) (s)
#include "fingerprint-strings.h"
#include "pam_fprintd_autoptrs.h"
#define DEFAULT_MAX_TRIES 3
#define DEFAULT_TIMEOUT 30
#define MIN_TIMEOUT 10
#define DEBUG_MATCH "debug="
#define MAX_TRIES_MATCH "max-tries="
#define TIMEOUT_MATCH "timeout="
static bool debug = false;
static unsigned max_tries = DEFAULT_MAX_TRIES;
static unsigned timeout = DEFAULT_TIMEOUT;
#define USEC_PER_SEC ((uint64_t) 1000000ULL)
#define NSEC_PER_USEC ((uint64_t) 1000ULL)
static uint64_t
now (void)
{
struct timespec ts;
clock_gettime (CLOCK_MONOTONIC, &ts);
return (uint64_t) ts.tv_sec * USEC_PER_SEC + (uint64_t) ts.tv_nsec / NSEC_PER_USEC;
}
static bool
str_has_prefix (const char *s, const char *prefix)
{
if (s == NULL || prefix == NULL)
return false;
return strncmp (s, prefix, strlen (prefix)) == 0;
}
static bool
send_msg (pam_handle_t *pamh, const char *msg, int style)
{
const struct pam_message mymsg = {
.msg_style = style,
.msg = msg,
};
const struct pam_message *msgp = &mymsg;
const struct pam_conv *pc;
struct pam_response *resp;
if (pam_get_item (pamh, PAM_CONV, (const void **) &pc) != PAM_SUCCESS)
return false;
if (!pc || !pc->conv)
return false;
return pc->conv (1, &msgp, &resp, pc->appdata_ptr) == PAM_SUCCESS;
}
static bool
send_info_msg (pam_handle_t *pamh, const char *msg)
{
return send_msg (pamh, msg, PAM_TEXT_INFO);
}
static bool
send_err_msg (pam_handle_t *pamh, const char *msg)
{
return send_msg (pamh, msg, PAM_ERROR_MSG);
}
static char *
open_device (pam_handle_t *pamh,
sd_bus *bus,
bool *has_multiple_devices)
{
pf_auto (sd_bus_error) error = SD_BUS_ERROR_NULL;
pf_autoptr (sd_bus_message) m = NULL;
size_t num_devices;
const char *path = NULL;
const char *s;
int r;
*has_multiple_devices = false;
if (sd_bus_call_method (bus,
"net.reactivated.Fprint",
"/net/reactivated/Fprint/Manager",
"net.reactivated.Fprint.Manager",
"GetDevices",
&error,
&m,
NULL) < 0)
{
pam_syslog (pamh, LOG_ERR, "GetDevices failed: %s", error.message);
return NULL;
}
r = sd_bus_message_enter_container (m, 'a', "o");
if (r < 0)
{
pam_syslog (pamh, LOG_ERR, "Failed to parse answer from GetDevices(): %d", r);
return NULL;
}
if (sd_bus_message_read_basic (m, 'o', &path) < 0)
return NULL;
num_devices = 1;
while ((r = sd_bus_message_read_basic (m, 'o', &s)) > 0)
num_devices++;
*has_multiple_devices = (num_devices > 1);
if (debug)
pam_syslog (pamh, LOG_DEBUG, "Using device %s (out of %ld devices)", path, num_devices);
sd_bus_message_exit_container (m);
return path ? strdup (path) : NULL;
}
typedef struct
{
char *dev;
bool has_multiple_devices;
unsigned max_tries;
char *result;
bool timed_out;
bool is_swipe;
bool verify_started;
int verify_ret;
pam_handle_t *pamh;
char *driver;
} verify_data;
static void
verify_data_free (verify_data *data)
{
free (data->result);
free (data->driver);
free (data->dev);
free (data);
}
PF_DEFINE_AUTOPTR_CLEANUP_FUNC (verify_data, verify_data_free)
static int
verify_result (sd_bus_message *m,
void *userdata,
sd_bus_error *ret_error)
{
verify_data *data = userdata;
const char *msg;
const char *result = NULL;
/* see https://github.com/systemd/systemd/issues/14643 */
uint64_t done = false;
int r;
if (!sd_bus_message_is_signal (m, "net.reactivated.Fprint.Device", "VerifyStatus"))
{
pam_syslog (data->pamh, LOG_ERR, "Not the signal we expected (iface: %s, member: %s)",
sd_bus_message_get_interface (m),
sd_bus_message_get_member (m));
return 0;
}
if ((r = sd_bus_message_read (m, "sb", &result, &done)) < 0)
{
pam_syslog (data->pamh, LOG_ERR, "Failed to parse VerifyResult signal: %d", r);
return 0;
}
if (!data->verify_started)
{
pam_syslog (data->pamh, LOG_ERR, "Unexpected VerifyResult '%s', %" PRIu64 " signal", result, done);
return 0;
}
if (debug)
pam_syslog (data->pamh, LOG_DEBUG, "Verify result: %s (done: %d)", result, done ? 1 : 0);
if (data->result)
{
free (data->result);
data->result = NULL;
}
if (done && result)
{
data->result = strdup (result);
return 0;
}
msg = verify_result_str_to_msg (result, data->is_swipe);
if (!msg)
{
data->result = strdup ("Protocol error with fprintd!");
return 0;
}
send_err_msg (data->pamh, msg);
return 0;
}
static int
verify_finger_selected (sd_bus_message *m,
void *userdata,
sd_bus_error *ret_error)
{
verify_data *data = userdata;
const char *finger_name = NULL;
pf_autofree char *msg = NULL;
if (sd_bus_message_read_basic (m, 's', &finger_name) < 0)
{
pam_syslog (data->pamh, LOG_ERR, "Failed to parse VerifyFingerSelected signal: %d", errno);
return 0;
}
if (!data->verify_started)
{
pam_syslog (data->pamh, LOG_ERR, "Unexpected VerifyFingerSelected %s signal", finger_name);
return 0;
}
msg = finger_str_to_msg (finger_name, data->driver, data->is_swipe);
if (!msg)
{
data->result = strdup ("Protocol error with fprintd!");
return 0;
}
if (debug)
pam_syslog (data->pamh, LOG_DEBUG, "verify_finger_selected %s", msg);
send_info_msg (data->pamh, msg);
return 0;
}
/* See https://github.com/systemd/systemd/issues/14636 */
static int
get_property_string (sd_bus *bus,
const char *destination,
const char *path,
const char *interface,
const char *member,
sd_bus_error *error,
char **ret)
{
pf_autoptr (sd_bus_message) reply = NULL;
const char *s;
char *n;
int r;
r = sd_bus_call_method (bus, destination, path, "org.freedesktop.DBus.Properties", "Get", error, &reply, "ss", interface, member);
if (r < 0)
return r;
r = sd_bus_message_enter_container (reply, 'v', "s");
if (r < 0)
return sd_bus_error_set_errno (error, r);
r = sd_bus_message_read_basic (reply, 's', &s);
if (r < 0)
return sd_bus_error_set_errno (error, r);
n = strdup (s);
if (!n)
return sd_bus_error_set_errno (error, -ENOMEM);
*ret = n;
return 0;
}
static int
verify_started_cb (sd_bus_message *m,
void *userdata,
sd_bus_error *ret_error)
{
const sd_bus_error *error = sd_bus_message_get_error (m);
verify_data *data = userdata;
if (error)
{
if (sd_bus_error_has_name (error, "net.reactivated.Fprint.Error.NoEnrolledPrints"))
{
pam_syslog (data->pamh, LOG_DEBUG, "No prints enrolled");
data->verify_ret = PAM_USER_UNKNOWN;
}
else
{
data->verify_ret = PAM_AUTH_ERR;
}
if (debug)
pam_syslog (data->pamh, LOG_DEBUG, "VerifyStart failed: %s", error->message);
return 1;
}
if (debug)
pam_syslog (data->pamh, LOG_DEBUG, "VerifyStart completed successfully");
data->verify_started = true;
return 1;
}
static int
do_verify (sd_bus *bus,
verify_data *data)
{
pf_autoptr (sd_bus_slot) verify_status_slot = NULL;
pf_autoptr (sd_bus_slot) verify_finger_selected_slot = NULL;
pf_autofree char *scan_type = NULL;
int r;
/* Get some properties for the device */
r = get_property_string (bus,
"net.reactivated.Fprint",
data->dev,
"net.reactivated.Fprint.Device",
"scan-type",
NULL,
&scan_type);
if (r < 0)
pam_syslog (data->pamh, LOG_ERR, "Failed to get scan-type for %s: %d", data->dev, r);
if (debug)
pam_syslog (data->pamh, LOG_DEBUG, "scan-type for %s: %s", data->dev, scan_type);
if (str_equal (scan_type, "swipe"))
data->is_swipe = true;
if (data->has_multiple_devices)
{
get_property_string (bus,
"net.reactivated.Fprint",
data->dev,
"net.reactivated.Fprint.Device",
"name",
NULL,
&data->driver);
if (r < 0)
pam_syslog (data->pamh, LOG_ERR, "Failed to get driver name for %s: %d", data->dev, r);
if (debug && r == 0)
pam_syslog (data->pamh, LOG_DEBUG, "driver name for %s: %s", data->dev, data->driver);
}
sd_bus_match_signal (bus,
&verify_status_slot,
"net.reactivated.Fprint",
data->dev,
"net.reactivated.Fprint.Device",
"VerifyStatus",
verify_result,
data);
sd_bus_match_signal (bus,
&verify_finger_selected_slot,
"net.reactivated.Fprint",
data->dev,
"net.reactivated.Fprint.Device",
"VerifyFingerSelected",
verify_finger_selected,
data);
while (data->max_tries > 0)
{
uint64_t verification_end = now () + (timeout * USEC_PER_SEC);
data->timed_out = false;
data->verify_started = false;
data->verify_ret = PAM_INCOMPLETE;
free (data->result);
data->result = NULL;
if (debug)
pam_syslog (data->pamh, LOG_DEBUG, "About to call VerifyStart");
r = sd_bus_call_method_async (bus,
NULL,
"net.reactivated.Fprint",
data->dev,
"net.reactivated.Fprint.Device",
"VerifyStart",
verify_started_cb,
data,
"s",
"any");
if (r < 0)
{
if (debug)
pam_syslog (data->pamh, LOG_DEBUG, "VerifyStart call failed: %d", r);
break;
}
for (;;)
{
int64_t wait_time;
wait_time = verification_end - now ();
if (wait_time <= 0)
break;
r = sd_bus_process (bus, NULL);
if (r < 0)
break;
if (data->verify_ret != PAM_INCOMPLETE)
break;
if (!data->verify_started)
continue;
if (data->result != NULL)
break;
if (r == 0)
{
if (debug)
{
pam_syslog (data->pamh, LOG_DEBUG,
"Waiting for %" PRId64 " seconds (%" PRId64 " usecs)",
wait_time / USEC_PER_SEC,
wait_time);
}
if (sd_bus_wait (bus, wait_time) < 0)
break;
}
}
if (data->verify_ret != PAM_INCOMPLETE)
return data->verify_ret;
if (now () >= verification_end)
{
data->timed_out = true;
send_info_msg (data->pamh, _("Verification timed out"));
}
/* Ignore errors from VerifyStop */
data->verify_started = false;
sd_bus_call_method (bus,
"net.reactivated.Fprint",
data->dev,
"net.reactivated.Fprint.Device",
"VerifyStop",
NULL,
NULL,
NULL,
NULL);
if (data->timed_out)
{
return PAM_AUTHINFO_UNAVAIL;
}
else
{
if (str_equal (data->result, "verify-no-match"))
{
send_err_msg (data->pamh, "Failed to match fingerprint");
}
else if (str_equal (data->result, "verify-match"))
{
return PAM_SUCCESS;
}
else if (str_equal (data->result, "verify-unknown-error"))
{
return PAM_AUTHINFO_UNAVAIL;
}
else if (str_equal (data->result, "verify-disconnected"))
{
return PAM_AUTHINFO_UNAVAIL;
}
else
{
send_err_msg (data->pamh, _("An unknown error occurred"));
return PAM_AUTH_ERR;
}
}
data->max_tries--;
}
if (data->max_tries == 0)
return PAM_MAXTRIES;
return PAM_AUTH_ERR;
}
static bool
user_has_prints (pam_handle_t *pamh,
sd_bus *bus,
const char *dev,
const char *username)
{
pf_auto (sd_bus_error) error = SD_BUS_ERROR_NULL;
pf_autoptr (sd_bus_message) m = NULL;
size_t num_fingers = 0;
const char *s;
int r;
r = sd_bus_call_method (bus,
"net.reactivated.Fprint",
dev,
"net.reactivated.Fprint.Device",
"ListEnrolledFingers",
&error,
&m,
"s",
username);
if (r < 0)
{
/* If ListEnrolledFingers fails then verification should
* also fail (both use the same underlying call), so we
* report false here and bail out early. */
if (debug)
pam_syslog (pamh, LOG_DEBUG, "ListEnrolledFingers failed for %s: %s",
username, error.message);
return false;
}
r = sd_bus_message_enter_container (m, 'a', "s");
if (r < 0)
{
pam_syslog (pamh, LOG_ERR, "Failed to parse answer from ListEnrolledFingers(): %d", r);
return false;
}
while ((r = sd_bus_message_read_basic (m, 's', &s)) > 0)
num_fingers++;
sd_bus_message_exit_container (m);
return num_fingers > 0;
}
static void
release_device (pam_handle_t *pamh,
sd_bus *bus,
const char *dev)
{
pf_auto (sd_bus_error) error = SD_BUS_ERROR_NULL;
if (sd_bus_call_method (bus,
"net.reactivated.Fprint",
dev,
"net.reactivated.Fprint.Device",
"Release",
&error,
NULL,
NULL,
NULL) < 0)
pam_syslog (pamh, LOG_ERR, "ReleaseDevice failed: %s", error.message);
}
static bool
claim_device (pam_handle_t *pamh,
sd_bus *bus,
const char *dev,
const char *username)
{
pf_auto (sd_bus_error) error = SD_BUS_ERROR_NULL;
if (sd_bus_call_method (bus,
"net.reactivated.Fprint",
dev,
"net.reactivated.Fprint.Device",
"Claim",
&error,
NULL,
"s",
username) < 0)
{
if (debug)
pam_syslog (pamh, LOG_DEBUG, "failed to claim device %s", error.message);
return false;
}
return true;
}
static int
name_owner_changed (sd_bus_message *m,
void *userdata,
sd_bus_error *ret_error)
{
verify_data *data = userdata;
const char *name = NULL;
const char *old_owner = NULL;
const char *new_owner = NULL;
if (sd_bus_message_read (m, "sss", &name, &old_owner, &new_owner) < 0)
{
pam_syslog (data->pamh, LOG_ERR, "Failed to parse NameOwnerChanged signal: %d", errno);
return 0;
}
if (strcmp (name, "net.reactivated.Fprint") != 0)
return 0;
/* Name owner for fprintd changed, give up as we might start listening
* to events from a new name owner otherwise. */
data->verify_ret = PAM_AUTHINFO_UNAVAIL;
pam_syslog (data->pamh, LOG_WARNING, "fprintd name owner changed during operation!");
return 0;
}
static int
do_auth (pam_handle_t *pamh, const char *username)
{
bool have_prints;
pf_autoptr (verify_data) data = NULL;
pf_autoptr (sd_bus) bus = NULL;
pf_autoptr (sd_bus_slot) name_owner_changed_slot = NULL;
data = calloc (1, sizeof (verify_data));
data->max_tries = max_tries;
data->pamh = pamh;
if (sd_bus_open_system (&bus) < 0)
{
pam_syslog (pamh, LOG_ERR, "Error with getting the bus: %d", errno);
return PAM_AUTHINFO_UNAVAIL;
}
data->dev = open_device (pamh, bus, &data->has_multiple_devices);
if (data->dev == NULL)
return PAM_AUTHINFO_UNAVAIL;
have_prints = user_has_prints (pamh, bus, data->dev, username);
if (debug)
pam_syslog (pamh, LOG_DEBUG, "prints registered: %s\n", have_prints ? "yes" : "no");
if (!have_prints)
return PAM_AUTHINFO_UNAVAIL;
/* Only connect to NameOwnerChanged when needed. In case of automatic startup
* we rely on the fact that we never see those signals.
*/
name_owner_changed_slot = NULL;
sd_bus_match_signal (bus,
&name_owner_changed_slot,
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
"NameOwnerChanged",
name_owner_changed,
data);
if (claim_device (pamh, bus, data->dev, username))
{
int ret = do_verify (bus, data);
release_device (pamh, bus, data->dev);
return ret;
}
return PAM_AUTHINFO_UNAVAIL;
}
static bool
is_remote (pam_handle_t *pamh)
{
const char *rhost = NULL;
pam_get_item (pamh, PAM_RHOST, (const void **) (const void *) &rhost);
/* NULL or empty rhost if the host information is not available or set.
* "localhost" if the host is local.
* We want to not run for known remote hosts */
if (rhost != NULL &&
*rhost != '\0' &&
strcmp (rhost, "localhost") != 0)
return true;
if (sd_session_is_remote (NULL) > 0)
return true;
return false;
}
PAM_EXTERN int
pam_sm_authenticate (pam_handle_t *pamh, int flags, int argc,
const char **argv)
{
const char *username;
int i;
bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
if (is_remote (pamh))
return PAM_AUTHINFO_UNAVAIL;
if (pam_get_user (pamh, &username, NULL) != PAM_SUCCESS)
return PAM_AUTHINFO_UNAVAIL;
for (i = 0; i < argc; i++)
{
if (argv[i] != NULL)
{
if (str_equal (argv[i], "debug"))
{
pam_syslog (pamh, LOG_DEBUG, "debug on");
debug = true;
}
else if (str_has_prefix (argv[i], DEBUG_MATCH))
{
pam_syslog (pamh, LOG_DEBUG, "debug on");
const char *value;
value = argv[i] + strlen (DEBUG_MATCH);
if (str_equal (value, "on") ||
str_equal (value, "true") ||
str_equal (value, "1"))
{
pam_syslog (pamh, LOG_DEBUG, "debug on");
debug = true;
}
else if (str_equal (value, "off") ||
str_equal (value, "false") ||
str_equal (value, "0"))
{
debug = false;
}
else
{
pam_syslog (pamh, LOG_DEBUG, "invalid debug value '%s', disabling", value);
}
}
else if (str_has_prefix (argv[i], MAX_TRIES_MATCH) && strlen (argv[i]) == strlen (MAX_TRIES_MATCH) + 1)
{
max_tries = atoi (argv[i] + strlen (MAX_TRIES_MATCH));
if (max_tries < 1)
{
if (debug)
pam_syslog (pamh, LOG_DEBUG, "invalid max tries '%s', using %d",
argv[i] + strlen (MAX_TRIES_MATCH), DEFAULT_MAX_TRIES);
max_tries = DEFAULT_MAX_TRIES;
}
if (debug)
pam_syslog (pamh, LOG_DEBUG, "max_tries specified as: %d", max_tries);
}
else if (str_has_prefix (argv[i], TIMEOUT_MATCH) && strlen (argv[i]) <= strlen (TIMEOUT_MATCH) + 2)
{
timeout = atoi (argv[i] + strlen (TIMEOUT_MATCH));
if (timeout < MIN_TIMEOUT)
{
if (debug)
pam_syslog (pamh, LOG_DEBUG, "timeout %d secs too low, using %d",
timeout, MIN_TIMEOUT);
timeout = MIN_TIMEOUT;
}
else if (debug)
{
pam_syslog (pamh, LOG_DEBUG, "timeout specified as: %d secs", timeout);
}
}
}
}
return do_auth (pamh, username);
}
PAM_EXTERN int
pam_sm_setcred (pam_handle_t *pamh, int flags, int argc,
const char **argv)
{
return PAM_SUCCESS;
}
PAM_EXTERN int
pam_sm_chauthtok (pam_handle_t *pamh, int flags, int argc,
const char **argv)
{
return PAM_SUCCESS;
}