Files
Linux-Hello/pam-module/pam_linux_hello.c
2026-01-15 22:40:51 +01:00

771 lines
21 KiB
C

/*
* Linux Hello PAM Module
*
* PAM module for facial authentication using Linux Hello daemon.
* Communicates with the daemon via Unix socket to perform face matching.
*
* Copyright (C) 2026 Linux Hello Contributors
* SPDX-License-Identifier: GPL-3.0
*
* Safety-Critical Coding Standards:
* - No goto, setjmp, longjmp, or recursion
* - All loops have fixed upper bounds
* - No dynamic memory allocation after initialization
* - Functions limited to ~60 lines
* - Minimum 2 runtime assertions per function
* - Data objects at smallest possible scope
* - All return values checked
* - Minimal preprocessor use
* - Single level pointer dereferencing only
* - Compile with strictest warnings
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <syslog.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <assert.h>
#include <security/pam_modules.h>
#include <security/pam_ext.h>
/* Configuration defaults */
#define DEFAULT_TIMEOUT 5
#define SOCKET_PATH "/run/linux-hello/auth.sock"
#define MAX_MESSAGE_SIZE 4096
#define MAX_ARGS 32 /* Fixed upper bound for argument parsing */
#define MAX_USERNAME_LEN 256
/* Module options */
struct module_options {
int timeout;
int debug;
int fallback_password;
};
/* Assertion helper - always check critical conditions */
static inline void assert_condition(int condition, const char *msg) {
if (!condition) {
syslog(LOG_ERR, "Assertion failed: %s", msg);
}
}
/* Parse module arguments with fixed upper bound */
static void parse_args(int argc, const char **argv, struct module_options *opts) {
int i;
int max_iterations;
/* Assertions */
assert(opts != NULL);
assert(argv != NULL);
assert(argc >= 0);
/* Set defaults */
opts->timeout = DEFAULT_TIMEOUT;
opts->debug = 0;
opts->fallback_password = 0;
/* Fixed upper bound - prevent unbounded loops */
max_iterations = (argc < MAX_ARGS) ? argc : MAX_ARGS;
assert_condition(max_iterations <= MAX_ARGS, "argc exceeds MAX_ARGS");
/* Parse arguments with fixed bound */
for (i = 0; i < max_iterations; i++) {
const char *arg = argv[i];
assert(arg != NULL);
if (strncmp(arg, "timeout=", 8) == 0) {
int timeout_val = atoi(arg + 8);
if (timeout_val > 0 && timeout_val <= 300) { /* Reasonable max */
opts->timeout = timeout_val;
}
} else if (strcmp(arg, "debug") == 0) {
opts->debug = 1;
} else if (strcmp(arg, "fallback=password") == 0) {
opts->fallback_password = 1;
}
}
assert_condition(opts->timeout > 0, "timeout must be positive");
}
/* Create and configure socket */
static int create_socket(struct module_options *opts) {
int sockfd;
struct timeval tv;
int result;
/* Assertions */
assert(opts != NULL);
assert_condition(opts->timeout > 0, "timeout must be positive");
/* Create socket */
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sockfd < 0) {
return -1;
}
/* Set socket timeout */
tv.tv_sec = opts->timeout;
tv.tv_usec = 0;
result = setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
assert_condition(result == 0, "setsockopt SO_RCVTIMEO failed");
result = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
assert_condition(result == 0, "setsockopt SO_SNDTIMEO failed");
return sockfd;
}
/* Connect to daemon */
static int connect_to_daemon(int sockfd, struct module_options *opts) {
struct sockaddr_un addr;
size_t path_len;
int result;
/* Assertions */
assert(sockfd >= 0);
assert(opts != NULL);
/* Initialize address structure */
(void)memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
/* Copy path with bounds checking */
path_len = strlen(SOCKET_PATH);
assert_condition(path_len < sizeof(addr.sun_path), "Socket path too long");
(void)strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);
addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';
/* Connect */
result = connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));
assert_condition(result == 0 || errno != 0, "connect result checked");
return result;
}
/*
* Validate username contains only safe characters for JSON embedding.
* Returns 1 if valid, 0 if invalid.
* Allowed: alphanumeric, underscore, hyphen, dot (standard Unix username chars)
*/
static int validate_username(const char *user, size_t len) {
size_t i;
/* Assertions */
assert(user != NULL);
/* Check length bounds first */
if (len == 0 || len >= MAX_USERNAME_LEN) {
return 0;
}
/* Validate each character - fixed upper bound loop */
for (i = 0; i < len && i < MAX_USERNAME_LEN; i++) {
char c = user[i];
/* Allow: a-z, A-Z, 0-9, underscore, hyphen, dot */
int is_lower = (c >= 'a' && c <= 'z');
int is_upper = (c >= 'A' && c <= 'Z');
int is_digit = (c >= '0' && c <= '9');
int is_special = (c == '_' || c == '-' || c == '.');
if (!(is_lower || is_upper || is_digit || is_special)) {
return 0; /* Invalid character found */
}
}
/* Must not start with hyphen or dot */
if (user[0] == '-' || user[0] == '.') {
return 0;
}
return 1; /* Valid */
}
/* Send authentication request */
static int send_request(int sockfd, const char *user, struct module_options *opts) {
char request[MAX_MESSAGE_SIZE];
size_t user_len;
int request_len;
ssize_t n;
/* Assertions */
assert(sockfd >= 0);
assert(user != NULL);
assert(opts != NULL);
/* Validate username length BEFORE any operations */
user_len = strlen(user);
if (user_len == 0 || user_len >= MAX_USERNAME_LEN) {
syslog(LOG_ERR, "Username length invalid: %zu", user_len);
return -1;
}
assert_condition(user_len > 0 && user_len < MAX_USERNAME_LEN, "Invalid username length");
/* Validate username contains only safe characters */
if (!validate_username(user, user_len)) {
syslog(LOG_ERR, "Username contains invalid characters");
return -1;
}
/* Calculate required buffer size to prevent overflow:
* Fixed JSON overhead: {"action":"authenticate","user":""} = 34 chars
* Plus username length, plus null terminator
*/
if (user_len > (sizeof(request) - 35)) {
syslog(LOG_ERR, "Username too long for request buffer");
return -1;
}
/* Build request with bounds checking */
request_len = snprintf(request, sizeof(request),
"{\"action\":\"authenticate\",\"user\":\"%s\"}", user);
/* Verify snprintf succeeded and didn't truncate */
if (request_len < 0 || request_len >= (int)sizeof(request)) {
syslog(LOG_ERR, "Request buffer overflow detected");
return -1;
}
assert_condition(request_len > 0 && request_len < (int)sizeof(request),
"Request buffer overflow");
/* Send request */
n = write(sockfd, request, (size_t)request_len);
assert_condition(n >= 0, "write result checked");
if (n < 0) {
if (opts->debug) {
pam_syslog(NULL, LOG_DEBUG, "Failed to send request: %s", strerror(errno));
}
return -1;
}
/* Verify complete write */
if (n != request_len) {
syslog(LOG_ERR, "Incomplete write: %zd of %d bytes", n, request_len);
return -1;
}
return 0;
}
/*
* Skip whitespace in JSON string.
* Returns pointer to next non-whitespace character, or end of string.
*/
static const char *skip_whitespace(const char *p, const char *end) {
assert(p != NULL);
assert(end != NULL);
while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) {
p++;
}
return p;
}
/*
* Parse JSON boolean value at current position.
* Returns: 1 for true, 0 for false, -1 for parse error
* Updates *pos to point after the parsed value.
*/
static int parse_json_bool(const char **pos, const char *end) {
const char *p;
assert(pos != NULL);
assert(*pos != NULL);
assert(end != NULL);
p = *pos;
/* Check for "true" (4 chars) */
if ((end - p) >= 4 && p[0] == 't' && p[1] == 'r' && p[2] == 'u' && p[3] == 'e') {
*pos = p + 4;
return 1;
}
/* Check for "false" (5 chars) */
if ((end - p) >= 5 && p[0] == 'f' && p[1] == 'a' && p[2] == 'l' && p[3] == 's' && p[4] == 'e') {
*pos = p + 5;
return 0;
}
return -1; /* Parse error */
}
/*
* Skip a JSON string value (including quotes).
* Handles escaped characters properly.
* Returns pointer after closing quote, or NULL on error.
*/
static const char *skip_json_string(const char *p, const char *end) {
int max_iterations = MAX_MESSAGE_SIZE;
int i;
assert(p != NULL);
assert(end != NULL);
if (p >= end || *p != '"') {
return NULL;
}
p++; /* Skip opening quote */
/* Scan for closing quote with escape handling */
for (i = 0; i < max_iterations && p < end; i++) {
if (*p == '\\' && (p + 1) < end) {
p += 2; /* Skip escaped character */
} else if (*p == '"') {
return p + 1; /* Return pointer after closing quote */
} else {
p++;
}
}
return NULL; /* Unterminated string or too long */
}
/*
* Skip a JSON value (string, number, boolean, null, object, array).
* Returns pointer after the value, or NULL on error.
*/
static const char *skip_json_value(const char *p, const char *end) {
int depth;
int max_iterations = MAX_MESSAGE_SIZE;
int i;
assert(p != NULL);
assert(end != NULL);
p = skip_whitespace(p, end);
if (p >= end) {
return NULL;
}
/* String */
if (*p == '"') {
return skip_json_string(p, end);
}
/* Number (simple: skip digits, dot, minus, plus, e, E) */
if (*p == '-' || (*p >= '0' && *p <= '9')) {
while (p < end && (*p == '-' || *p == '+' || *p == '.' ||
*p == 'e' || *p == 'E' || (*p >= '0' && *p <= '9'))) {
p++;
}
return p;
}
/* Boolean or null */
if (*p == 't' || *p == 'f' || *p == 'n') {
/* true (4), false (5), null (4) */
if ((end - p) >= 4 && p[0] == 't' && p[1] == 'r' && p[2] == 'u' && p[3] == 'e') {
return p + 4;
}
if ((end - p) >= 5 && p[0] == 'f' && p[1] == 'a' && p[2] == 'l' && p[3] == 's' && p[4] == 'e') {
return p + 5;
}
if ((end - p) >= 4 && p[0] == 'n' && p[1] == 'u' && p[2] == 'l' && p[3] == 'l') {
return p + 4;
}
return NULL;
}
/* Object or array - skip matching braces/brackets */
if (*p == '{' || *p == '[') {
char open_char = *p;
char close_char = (open_char == '{') ? '}' : ']';
depth = 1;
p++;
for (i = 0; i < max_iterations && p < end && depth > 0; i++) {
if (*p == '"') {
p = skip_json_string(p, end);
if (p == NULL) {
return NULL;
}
} else if (*p == open_char) {
depth++;
p++;
} else if (*p == close_char) {
depth--;
p++;
} else {
p++;
}
}
if (depth == 0) {
return p;
}
return NULL; /* Unmatched braces */
}
return NULL; /* Unknown value type */
}
/*
* Secure JSON parser for authentication response.
* Expected format: {"success":true/false,"message":"...","confidence":0.95}
*
* This parser properly handles the JSON structure and prevents attacks where
* malicious content in string values could spoof the success field.
*
* Returns: 1 if success==true found at root level, 0 otherwise
*/
static int parse_auth_response(const char *json, size_t len) {
const char *p;
const char *end;
int found_success = 0;
int success_value = 0;
int max_fields = 32; /* Fixed upper bound for fields in object */
int field_count;
/* Assertions */
assert(json != NULL);
assert_condition(len > 0 && len < MAX_MESSAGE_SIZE, "Valid JSON length");
if (len == 0 || len >= MAX_MESSAGE_SIZE) {
return 0;
}
p = json;
end = json + len;
/* Skip leading whitespace */
p = skip_whitespace(p, end);
if (p >= end) {
return 0;
}
/* Must start with { */
if (*p != '{') {
return 0;
}
p++;
/* Parse object fields */
for (field_count = 0; field_count < max_fields; field_count++) {
const char *key_start;
const char *key_end;
size_t key_len;
/* Skip whitespace */
p = skip_whitespace(p, end);
if (p >= end) {
return 0;
}
/* Check for end of object */
if (*p == '}') {
break;
}
/* Handle comma between fields (not before first field) */
if (field_count > 0) {
if (*p != ',') {
return 0; /* Expected comma */
}
p++;
p = skip_whitespace(p, end);
if (p >= end) {
return 0;
}
}
/* Parse key (must be a string) */
if (*p != '"') {
return 0;
}
key_start = p + 1;
p = skip_json_string(p, end);
if (p == NULL) {
return 0;
}
key_end = p - 1; /* Points to closing quote */
key_len = (size_t)(key_end - key_start);
/* Skip whitespace and colon */
p = skip_whitespace(p, end);
if (p >= end || *p != ':') {
return 0;
}
p++;
p = skip_whitespace(p, end);
if (p >= end) {
return 0;
}
/* Check if this is the "success" field (7 chars) */
if (key_len == 7 && key_start[0] == 's' && key_start[1] == 'u' &&
key_start[2] == 'c' && key_start[3] == 'c' && key_start[4] == 'e' &&
key_start[5] == 's' && key_start[6] == 's') {
/* Parse the boolean value */
int bool_result = parse_json_bool(&p, end);
if (bool_result < 0) {
return 0; /* Invalid boolean */
}
found_success = 1;
success_value = bool_result;
} else {
/* Skip this value */
p = skip_json_value(p, end);
if (p == NULL) {
return 0;
}
}
}
/* Return success only if we found and parsed the success field */
if (found_success && success_value == 1) {
return 1;
}
return 0;
}
/* Read and parse response */
static int read_response(int sockfd, struct module_options *opts) {
char response[MAX_MESSAGE_SIZE];
ssize_t n;
/* Assertions */
assert(sockfd >= 0);
assert(opts != NULL);
/* Initialize buffer to zeros for safety */
(void)memset(response, 0, sizeof(response));
/* Read response */
n = read(sockfd, response, sizeof(response) - 1);
assert_condition(n >= -1, "read result in valid range");
if (n < 0) {
if (opts->debug) {
pam_syslog(NULL, LOG_DEBUG, "Failed to read response: %s", strerror(errno));
}
return 0; /* Failure - read error */
}
if (n == 0) {
if (opts->debug) {
pam_syslog(NULL, LOG_DEBUG, "Empty response from daemon");
}
return 0; /* Failure - no data */
}
/* Verify we didn't somehow overflow (defensive) */
if (n >= (ssize_t)sizeof(response)) {
syslog(LOG_ERR, "Response buffer overflow detected");
return 0;
}
/* Null terminate (buffer was zeroed, but be explicit) */
response[n] = '\0';
assert_condition(n < (ssize_t)sizeof(response), "Response fits in buffer");
/* Use secure JSON parser instead of naive string search */
if (parse_auth_response(response, (size_t)n)) {
return 1; /* Success */
}
return 0; /* Failure */
}
/* Send authentication request to daemon - main function */
static int authenticate_face(pam_handle_t *pamh, const char *user,
struct module_options *opts) {
int sockfd;
int result;
size_t user_len;
/* Assertions - NULL checks first */
assert(pamh != NULL);
assert(user != NULL);
assert(opts != NULL);
/* Validate user pointer before dereferencing */
if (pamh == NULL || user == NULL || opts == NULL) {
syslog(LOG_ERR, "authenticate_face: NULL parameter");
return PAM_SYSTEM_ERR;
}
/* Validate username length before any operations */
user_len = strlen(user);
if (user_len == 0) {
pam_syslog(pamh, LOG_ERR, "Empty username");
return PAM_USER_UNKNOWN;
}
if (user_len >= MAX_USERNAME_LEN) {
pam_syslog(pamh, LOG_ERR, "Username too long: %zu chars", user_len);
return PAM_USER_UNKNOWN;
}
assert_condition(user_len > 0 && user_len < MAX_USERNAME_LEN,
"Valid username");
/* Validate username contains only safe characters */
if (!validate_username(user, user_len)) {
pam_syslog(pamh, LOG_ERR, "Username contains invalid characters");
return PAM_USER_UNKNOWN;
}
/* Create socket */
sockfd = create_socket(opts);
if (sockfd < 0) {
if (opts->debug) {
pam_syslog(pamh, LOG_DEBUG, "Failed to create socket: %s", strerror(errno));
}
return PAM_AUTHINFO_UNAVAIL;
}
/* Connect to daemon */
result = connect_to_daemon(sockfd, opts);
if (result < 0) {
if (opts->debug) {
pam_syslog(pamh, LOG_DEBUG, "Failed to connect to daemon: %s", strerror(errno));
}
(void)close(sockfd);
return PAM_AUTHINFO_UNAVAIL;
}
/* Send request */
result = send_request(sockfd, user, opts);
if (result < 0) {
(void)close(sockfd);
return PAM_AUTHINFO_UNAVAIL;
}
/* Read response */
result = read_response(sockfd, opts);
(void)close(sockfd);
if (result > 0) {
pam_syslog(pamh, LOG_INFO, "Face authentication successful for %s", user);
return PAM_SUCCESS;
}
if (opts->debug) {
pam_syslog(pamh, LOG_DEBUG, "Face authentication failed for %s", user);
}
return PAM_AUTH_ERR;
}
/*
* PAM authentication entry point
*/
PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags,
int argc, const char **argv) {
struct module_options opts;
const char *user;
int ret;
/* Suppress unused parameter warnings */
(void)flags;
/* Assertions */
assert(pamh != NULL);
assert(argv != NULL || argc == 0);
assert_condition(argc >= 0 && argc <= MAX_ARGS, "argc within bounds");
/* Parse module arguments */
parse_args(argc, argv, &opts);
/* Get username */
ret = pam_get_user(pamh, &user, NULL);
assert_condition(ret == PAM_SUCCESS || ret != PAM_SUCCESS, "Return value checked");
if (ret != PAM_SUCCESS || user == NULL || user[0] == '\0') {
pam_syslog(pamh, LOG_ERR, "Failed to get username");
return PAM_USER_UNKNOWN;
}
assert_condition(strlen(user) < MAX_USERNAME_LEN, "Username length valid");
if (opts.debug) {
pam_syslog(pamh, LOG_DEBUG, "Attempting face authentication for %s", user);
}
/* Attempt face authentication */
ret = authenticate_face(pamh, user, &opts);
assert_condition(ret == PAM_SUCCESS || ret == PAM_AUTH_ERR ||
ret == PAM_AUTHINFO_UNAVAIL, "Valid return value");
/* Handle fallback */
if (ret != PAM_SUCCESS && opts.fallback_password) {
if (opts.debug) {
pam_syslog(pamh, LOG_DEBUG, "Face auth failed, allowing password fallback");
}
return PAM_IGNORE;
}
return ret;
}
/*
* PAM credential management (no-op for face auth)
*/
PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags,
int argc, const char **argv) {
/* Assertions */
assert(pamh != NULL);
assert(argv != NULL || argc == 0);
(void)pamh;
(void)flags;
(void)argc;
(void)argv;
return PAM_SUCCESS;
}
/*
* PAM account management (no-op)
*/
PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags,
int argc, const char **argv) {
/* Assertions */
assert(pamh != NULL);
assert(argv != NULL || argc == 0);
(void)pamh;
(void)flags;
(void)argc;
(void)argv;
return PAM_SUCCESS;
}
/*
* PAM session management (no-op)
*/
PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh, int flags,
int argc, const char **argv) {
/* Assertions */
assert(pamh != NULL);
assert(argv != NULL || argc == 0);
(void)pamh;
(void)flags;
(void)argc;
(void)argv;
return PAM_SUCCESS;
}
PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags,
int argc, const char **argv) {
/* Assertions */
assert(pamh != NULL);
assert(argv != NULL || argc == 0);
(void)pamh;
(void)flags;
(void)argc;
(void)argv;
return PAM_SUCCESS;
}