pam-redbear: port minimal PAM to Redox; proxy to redbear-authd
This commit is contained in:
@@ -63,9 +63,10 @@ redox-drm = {}
|
||||
mesa = {}
|
||||
libdrm = {}
|
||||
libepoxy = {}
|
||||
libdisplay-info = "ignore"
|
||||
libxcvt = "ignore"
|
||||
lcms2 = "ignore"
|
||||
libdisplay-info = {}
|
||||
libxcvt = {}
|
||||
lcms2 = {}
|
||||
libudev = {}
|
||||
freetype2 = {}
|
||||
fontconfig = {}
|
||||
|
||||
@@ -153,6 +154,7 @@ redbear-authd = {}
|
||||
redbear-session-launch = {}
|
||||
seatd = {}
|
||||
redbear-greeter = {}
|
||||
pam-redbear = {}
|
||||
sddm = {}
|
||||
amdgpu = "ignore"
|
||||
#amdgpu = {} # TODO: fix conflicting idr_* defs with linux-kpi headers
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
# PAM compatibility shim — implements the PAM API by delegating authentication
|
||||
# to redbear-authd via Unix socket JSON protocol. Provides libpam.so and
|
||||
# security/pam_appl.h for SDDM and other PAM-consuming software.
|
||||
# PAM compatibility library for Red Bear OS (v6.0 2026)
|
||||
#
|
||||
# Implements the C PAM ABI expected by SDDM and other PAM consumers and
|
||||
# proxies authentication requests to redbear-authd over its Unix socket
|
||||
# protocol. Built as a Rust `cdylib` (libpam.so.0) per the project's
|
||||
# Rust-first policy for system-critical infrastructure.
|
||||
[source]
|
||||
path = "source"
|
||||
|
||||
[build]
|
||||
template = "custom"
|
||||
# redbear-authd is a runtime dependency (we talk to its Unix socket at
|
||||
# authentication time). redbear-login-protocol is a Cargo path dependency
|
||||
# declared in source/Cargo.toml — the cookbook must not try to install it
|
||||
# as a separate recipe because it is a lib-only crate with no [[bin]].
|
||||
dependencies = [
|
||||
"redbear-authd",
|
||||
]
|
||||
@@ -14,25 +21,24 @@ DYNAMIC_INIT
|
||||
|
||||
mkdir -p "${COOKBOOK_STAGE}/usr/lib"
|
||||
mkdir -p "${COOKBOOK_STAGE}/usr/include/security"
|
||||
mkdir -p "${COOKBOOK_STAGE}/usr/lib/pkgconfig"
|
||||
|
||||
x86_64-unknown-redox-gcc \
|
||||
-shared \
|
||||
-fPIC \
|
||||
-std=c11 \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wl,-soname,libpam.so.0 \
|
||||
-I"${COOKBOOK_SOURCE}" \
|
||||
-o "${COOKBOOK_STAGE}/usr/lib/libpam.so.0.99.0" \
|
||||
"${COOKBOOK_SOURCE}/pam_redbear.c"
|
||||
reexport_flags
|
||||
"${COOKBOOK_CARGO}" build \
|
||||
--manifest-path "${COOKBOOK_SOURCE}/Cargo.toml" \
|
||||
--lib \
|
||||
${build_flags} \
|
||||
-j "${COOKBOOK_MAKE_JOBS}" \
|
||||
${COOKBOOK_CARGO_FLAGS[@]}
|
||||
|
||||
ln -sf libpam.so.0.99.0 "${COOKBOOK_STAGE}/usr/lib/libpam.so.0"
|
||||
cp "${COOKBOOK_BUILD}/target/${TARGET}/release/libpam.so" \
|
||||
"${COOKBOOK_STAGE}/usr/lib/libpam.so.0.2.3"
|
||||
ln -sf libpam.so.0.2.3 "${COOKBOOK_STAGE}/usr/lib/libpam.so.0"
|
||||
ln -sf libpam.so.0 "${COOKBOOK_STAGE}/usr/lib/libpam.so"
|
||||
|
||||
cp "${COOKBOOK_SOURCE}/security/pam_appl.h" "${COOKBOOK_STAGE}/usr/include/security/pam_appl.h"
|
||||
cp "${COOKBOOK_SOURCE}/include/security/pam_appl.h" \
|
||||
"${COOKBOOK_STAGE}/usr/include/security/pam_appl.h"
|
||||
|
||||
# pkg-config for downstream cmake/find_package lookups
|
||||
mkdir -p "${COOKBOOK_STAGE}/usr/lib/pkgconfig"
|
||||
cat > "${COOKBOOK_STAGE}/usr/lib/pkgconfig/pam.pc" << 'EOF'
|
||||
prefix=/usr
|
||||
exec_prefix=${prefix}
|
||||
@@ -40,9 +46,16 @@ libdir=${exec_prefix}/lib
|
||||
includedir=${prefix}/include
|
||||
|
||||
Name: pam
|
||||
Description: PAM compatibility shim (redbear-authd backend)
|
||||
Version: 0.99.0
|
||||
Description: PAM compatibility library for Red Bear OS — proxies authentication to redbear-authd
|
||||
Version: 0.2.3
|
||||
Libs: -L${libdir} -lpam
|
||||
Cflags: -I${includedir}
|
||||
EOF
|
||||
"""
|
||||
|
||||
[package]
|
||||
version = "0.2.3"
|
||||
# Files are installed by the script above; no [package.files] mapping
|
||||
# needed for the cdylib itself. The version is bumped to 0.2.3 to match
|
||||
# the in-tree package version and to make the SONAME upgrade visible
|
||||
# to any existing consumers (SDDM, su, etc.).
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "pam"
|
||||
version = "0.2.3"
|
||||
edition = "2021"
|
||||
description = "PAM compatibility library for Red Bear OS — proxies authentication to redbear-authd over its Unix socket protocol. v6.0 2026"
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
name = "pam"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# Enable to talk to redbear-authd over the real /run/redbear-authd.sock.
|
||||
# Disable for unit tests that exercise the API in isolation.
|
||||
authd-socket = []
|
||||
|
||||
[dependencies]
|
||||
redbear-login-protocol = { path = "../../../system/redbear-login-protocol/source" }
|
||||
serde_json = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1"
|
||||
+49
-36
@@ -9,13 +9,13 @@ extern "C" {
|
||||
|
||||
typedef int pam_item_type;
|
||||
|
||||
#define PAM_SUCCESS 0
|
||||
#define PAM_OPEN_ERR 1
|
||||
#define PAM_SYMBOL_ERR 2
|
||||
#define PAM_SERVICE_ERR 3
|
||||
#define PAM_SYSTEM_ERR 4
|
||||
#define PAM_BUF_ERR 5
|
||||
#define PAM_PERM_DENIED 6
|
||||
#define PAM_SUCCESS 0
|
||||
#define PAM_OPEN_ERR 1
|
||||
#define PAM_SYMBOL_ERR 2
|
||||
#define PAM_SERVICE_ERR 3
|
||||
#define PAM_SYSTEM_ERR 4
|
||||
#define PAM_BUF_ERR 5
|
||||
#define PAM_PERM_DENIED 6
|
||||
#define PAM_AUTH_ERR 7
|
||||
#define PAM_CRED_INSUFFICIENT 8
|
||||
#define PAM_AUTHINFO_UNAVAIL 9
|
||||
@@ -30,37 +30,45 @@ typedef int pam_item_type;
|
||||
#define PAM_NO_MODULE_DATA 18
|
||||
#define PAM_CONV_ERR 19
|
||||
#define PAM_AUTHTOK_ERR 20
|
||||
#define PAM_ABORT 21
|
||||
#define PAM_AUTHTOK_RECOVER_ERR 22
|
||||
#define PAM_AUTHTOK_LOCK_BUSY 23
|
||||
#define PAM_AUTHTOK_DISABLE_AGING 24
|
||||
#define PAM_TRY_AGAIN 25
|
||||
#define PAM_IGNORE 26
|
||||
#define PAM_MODULE_UNKNOWN 27
|
||||
#define PAM_AUTHTOK_EXPIRED 28
|
||||
#define PAM_AUTHTOK_RECOVER_ERR 21
|
||||
#define PAM_AUTHTOK_LOCK_BUSY 22
|
||||
#define PAM_AUTHTOK_DISABLE_AGING 23
|
||||
#define PAM_TRY_AGAIN 24
|
||||
#define PAM_IGNORE 25
|
||||
#define PAM_ABORT 26
|
||||
#define PAM_AUTHTOK_EXPIRED 27
|
||||
#define PAM_BAD_ITEM 29
|
||||
#define PAM_BAD_STATE 30
|
||||
#define PAM_NO_MODULE_DATA_CRIT 31
|
||||
|
||||
#define PAM_SERVICE 1
|
||||
#define PAM_USER 2
|
||||
#define PAM_TTY 3
|
||||
#define PAM_RHOST 4
|
||||
#define PAM_CONV 5
|
||||
#define PAM_RUSER 8
|
||||
#define PAM_SERVICE 1
|
||||
#define PAM_USER 2
|
||||
#define PAM_TTY 3
|
||||
#define PAM_RHOST 4
|
||||
#define PAM_CONV 5
|
||||
#define PAM_AUTHTOK 6
|
||||
#define PAM_OLDAUTHTOK 7
|
||||
#define PAM_RUSER 8
|
||||
#define PAM_USER_PROMPT 9
|
||||
#define PAM_FAIL_DELAY 10
|
||||
#define PAM_XDISPLAY 11
|
||||
#define PAM_XAUTHDATA 12
|
||||
#define PAM_AUTHTOK_TYPE 13
|
||||
|
||||
#define PAM_SILENT 0x8000
|
||||
#define PAM_DISALLOW_NULL_AUTHTOK 0x0001
|
||||
#define PAM_ESTABLISH_CRED 0x0002
|
||||
#define PAM_DELETE_CRED 0x0004
|
||||
#define PAM_REINITIALIZE_CRED 0x0008
|
||||
#define PAM_REFRESH_CRED 0x0010
|
||||
#define PAM_CHANGE_EXPIRED_AUTHTOK 0x0020
|
||||
#define PAM_SILENT 0x8000
|
||||
#define PAM_DISALLOW_NULL_AUTHTOK 0x0001
|
||||
#define PAM_ESTABLISH_CRED 0x0002
|
||||
#define PAM_DELETE_CRED 0x0004
|
||||
#define PAM_REINITIALIZE_CRED 0x0008
|
||||
#define PAM_REFRESH_CRED 0x0010
|
||||
#define PAM_CHANGE_EXPIRED_AUTHTOK 0x0020
|
||||
|
||||
#define PAM_MAX_NUM_MSG 32
|
||||
#define PAM_MAX_NUM_MSG 32
|
||||
|
||||
#define PAM_PROMPT_ECHO_OFF 1
|
||||
#define PAM_PROMPT_ECHO_ON 2
|
||||
#define PAM_ERROR_MSG 3
|
||||
#define PAM_TEXT_INFO 4
|
||||
#define PAM_PROMPT_ECHO_OFF 1
|
||||
#define PAM_PROMPT_ECHO_ON 2
|
||||
#define PAM_ERROR_MSG 3
|
||||
#define PAM_TEXT_INFO 4
|
||||
|
||||
struct pam_message {
|
||||
int msg_style;
|
||||
@@ -80,7 +88,7 @@ struct pam_conv {
|
||||
|
||||
typedef struct pam_handle pam_handle_t;
|
||||
|
||||
int pam_start(const char *service, const char *user,
|
||||
int pam_start(const char *service_name, const char *user,
|
||||
const struct pam_conv *conv, pam_handle_t **pamh);
|
||||
|
||||
int pam_end(pam_handle_t *pamh, int pam_status);
|
||||
@@ -89,6 +97,11 @@ int pam_set_item(pam_handle_t *pamh, int item_type, const void *item);
|
||||
|
||||
int pam_get_item(const pam_handle_t *pamh, int item_type, const void **item);
|
||||
|
||||
int pam_get_data(const pam_handle_t *pamh, const char *module_data_name, void **data);
|
||||
|
||||
int pam_set_data(pam_handle_t *pamh, const char *module_data_name, void *data,
|
||||
void (*cleanup)(pam_handle_t *pamh, void *data, int error_status));
|
||||
|
||||
int pam_authenticate(pam_handle_t *pamh, int flags);
|
||||
|
||||
int pam_acct_mgmt(pam_handle_t *pamh, int flags);
|
||||
@@ -97,12 +110,12 @@ int pam_open_session(pam_handle_t *pamh, int flags);
|
||||
|
||||
int pam_close_session(pam_handle_t *pamh, int flags);
|
||||
|
||||
const char *pam_strerror(pam_handle_t *pamh, int errnum);
|
||||
|
||||
int pam_setcred(pam_handle_t *pamh, int flags);
|
||||
|
||||
int pam_chauthtok(pam_handle_t *pamh, int flags);
|
||||
|
||||
const char *pam_strerror(pam_handle_t *pamh, int errnum);
|
||||
|
||||
int pam_putenv(pam_handle_t *pamh, const char *name_value);
|
||||
|
||||
char **pam_getenvlist(pam_handle_t *pamh);
|
||||
@@ -1,342 +0,0 @@
|
||||
#include <security/pam_appl.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define AUTHD_SOCK_PATH "/run/redbear-authd.sock"
|
||||
#define AUTHD_BUF_SIZE 4096
|
||||
#define AUTHD_RESP_MAX 1024
|
||||
|
||||
struct pam_handle {
|
||||
char *service;
|
||||
char *user;
|
||||
char *tty;
|
||||
char *rhost;
|
||||
char *ruser;
|
||||
struct pam_conv conv;
|
||||
};
|
||||
|
||||
static int connect_authd(void)
|
||||
{
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
|
||||
struct sockaddr_un addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sun_family = AF_UNIX;
|
||||
strncpy(addr.sun_path, AUTHD_SOCK_PATH, sizeof(addr.sun_path) - 1);
|
||||
|
||||
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
static ssize_t send_all(int fd, const char *buf, size_t len)
|
||||
{
|
||||
size_t sent = 0;
|
||||
while (sent < len) {
|
||||
ssize_t n = send(fd, buf + sent, len - sent, 0);
|
||||
if (n < 0) return -1;
|
||||
sent += (size_t)n;
|
||||
}
|
||||
return (ssize_t)sent;
|
||||
}
|
||||
|
||||
static ssize_t recv_line(int fd, char *buf, size_t bufsize)
|
||||
{
|
||||
size_t total = 0;
|
||||
while (total < bufsize - 1) {
|
||||
ssize_t n = recv(fd, buf + total, 1, 0);
|
||||
if (n <= 0) return n < 0 ? -1 : (ssize_t)total;
|
||||
total += (size_t)n;
|
||||
if (buf[total - 1] == '\n') break;
|
||||
}
|
||||
buf[total] = '\0';
|
||||
return (ssize_t)total;
|
||||
}
|
||||
|
||||
__attribute__((unused))
|
||||
static char *json_extract_string(const char *json, const char *key)
|
||||
{
|
||||
char search[128];
|
||||
snprintf(search, sizeof(search), "\"%s\":", key);
|
||||
|
||||
const char *pos = strstr(json, search);
|
||||
if (!pos) return NULL;
|
||||
|
||||
pos += strlen(search);
|
||||
while (*pos == ' ' || *pos == '\t') pos++;
|
||||
|
||||
if (*pos != '"') return NULL;
|
||||
pos++;
|
||||
|
||||
size_t klen = strlen(key);
|
||||
(void)klen;
|
||||
const char *end = strchr(pos, '"');
|
||||
if (!end) return NULL;
|
||||
|
||||
size_t vlen = (size_t)(end - pos);
|
||||
char *val = malloc(vlen + 1);
|
||||
if (!val) return NULL;
|
||||
memcpy(val, pos, vlen);
|
||||
val[vlen] = '\0';
|
||||
return val;
|
||||
}
|
||||
|
||||
static int json_extract_bool(const char *json, const char *key)
|
||||
{
|
||||
char search[128];
|
||||
snprintf(search, sizeof(search), "\"%s\":", key);
|
||||
|
||||
const char *pos = strstr(json, search);
|
||||
if (!pos) return -1;
|
||||
|
||||
pos += strlen(search);
|
||||
while (*pos == ' ' || *pos == '\t') pos++;
|
||||
|
||||
if (strncmp(pos, "true", 4) == 0) return 1;
|
||||
if (strncmp(pos, "false", 5) == 0) return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int pam_start(const char *service, const char *user,
|
||||
const struct pam_conv *conv, pam_handle_t **pamh)
|
||||
{
|
||||
if (!pamh) return PAM_SYSTEM_ERR;
|
||||
|
||||
pam_handle_t *h = calloc(1, sizeof(*h));
|
||||
if (!h) return PAM_BUF_ERR;
|
||||
|
||||
if (service) {
|
||||
h->service = strdup(service);
|
||||
if (!h->service) { free(h); return PAM_BUF_ERR; }
|
||||
}
|
||||
if (user) {
|
||||
h->user = strdup(user);
|
||||
if (!h->user) { free(h->service); free(h); return PAM_BUF_ERR; }
|
||||
}
|
||||
if (conv) {
|
||||
h->conv = *conv;
|
||||
}
|
||||
|
||||
*pamh = h;
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
|
||||
int pam_end(pam_handle_t *pamh, int pam_status)
|
||||
{
|
||||
(void)pam_status;
|
||||
if (!pamh) return PAM_SYSTEM_ERR;
|
||||
|
||||
free(pamh->service);
|
||||
free(pamh->user);
|
||||
free(pamh->tty);
|
||||
free(pamh->rhost);
|
||||
free(pamh->ruser);
|
||||
free(pamh);
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
|
||||
int pam_set_item(pam_handle_t *pamh, int item_type, const void *item)
|
||||
{
|
||||
if (!pamh) return PAM_SYSTEM_ERR;
|
||||
|
||||
char **target = NULL;
|
||||
switch (item_type) {
|
||||
case PAM_SERVICE: target = &pamh->service; break;
|
||||
case PAM_USER: target = &pamh->user; break;
|
||||
case PAM_TTY: target = &pamh->tty; break;
|
||||
case PAM_RHOST: target = &pamh->rhost; break;
|
||||
case PAM_RUSER: target = &pamh->ruser; break;
|
||||
case PAM_CONV:
|
||||
if (item) pamh->conv = *(const struct pam_conv *)item;
|
||||
return PAM_SUCCESS;
|
||||
default:
|
||||
return PAM_BAD_ITEM;
|
||||
}
|
||||
|
||||
if (!target) return PAM_BAD_ITEM;
|
||||
|
||||
free(*target);
|
||||
*target = NULL;
|
||||
if (item) {
|
||||
*target = strdup((const char *)item);
|
||||
if (!*target) return PAM_BUF_ERR;
|
||||
}
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
|
||||
int pam_get_item(const pam_handle_t *pamh, int item_type, const void **item)
|
||||
{
|
||||
if (!pamh || !item) return PAM_SYSTEM_ERR;
|
||||
|
||||
switch (item_type) {
|
||||
case PAM_SERVICE: *item = pamh->service; break;
|
||||
case PAM_USER: *item = pamh->user; break;
|
||||
case PAM_TTY: *item = pamh->tty; break;
|
||||
case PAM_RHOST: *item = pamh->rhost; break;
|
||||
case PAM_RUSER: *item = pamh->ruser; break;
|
||||
case PAM_CONV: *item = &pamh->conv; break;
|
||||
default:
|
||||
*item = NULL;
|
||||
return PAM_BAD_ITEM;
|
||||
}
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
|
||||
int pam_authenticate(pam_handle_t *pamh, int flags)
|
||||
{
|
||||
(void)flags;
|
||||
|
||||
if (!pamh) return PAM_SYSTEM_ERR;
|
||||
if (!pamh->conv.conv) return PAM_SYSTEM_ERR;
|
||||
|
||||
const char *username = pamh->user;
|
||||
if (!username) return PAM_AUTH_ERR;
|
||||
|
||||
const struct pam_message msg = {
|
||||
.msg_style = PAM_PROMPT_ECHO_OFF,
|
||||
.msg = "Password: "
|
||||
};
|
||||
const struct pam_message *msg_ptr = &msg;
|
||||
struct pam_response *resp = NULL;
|
||||
|
||||
int rc = pamh->conv.conv(1, &msg_ptr, &resp, pamh->conv.appdata_ptr);
|
||||
if (rc != PAM_SUCCESS || !resp || !resp[0].resp) return PAM_CONV_ERR;
|
||||
|
||||
const char *password = resp[0].resp;
|
||||
|
||||
int fd = connect_authd();
|
||||
if (fd < 0) {
|
||||
if (resp[0].resp) { memset(resp[0].resp, 0, strlen(resp[0].resp)); free(resp[0].resp); }
|
||||
free(resp);
|
||||
return PAM_AUTHINFO_UNAVAIL;
|
||||
}
|
||||
|
||||
char request[AUTHD_BUF_SIZE];
|
||||
int n = snprintf(request, sizeof(request),
|
||||
"{\"type\":\"Authenticate\",\"username\":\"%s\",\"password\":\"",
|
||||
username);
|
||||
|
||||
for (const char *p = password; *p && (size_t)n < sizeof(request) - 4; p++) {
|
||||
if (*p == '"' || *p == '\\') {
|
||||
request[n++] = '\\';
|
||||
if ((size_t)n >= sizeof(request) - 3) break;
|
||||
}
|
||||
request[n++] = *p;
|
||||
}
|
||||
n += snprintf(request + n, sizeof(request) - (size_t)n, "\",\"vt\":0}\n");
|
||||
|
||||
memset((void *)password, 0, strlen(password));
|
||||
free(resp[0].resp);
|
||||
free(resp);
|
||||
|
||||
if (send_all(fd, request, (size_t)n) < 0) {
|
||||
close(fd);
|
||||
return PAM_AUTH_ERR;
|
||||
}
|
||||
|
||||
char response[AUTHD_RESP_MAX];
|
||||
ssize_t rlen = recv_line(fd, response, sizeof(response));
|
||||
close(fd);
|
||||
|
||||
if (rlen <= 0) return PAM_AUTH_ERR;
|
||||
|
||||
int ok = json_extract_bool(response, "ok");
|
||||
if (ok == 1) return PAM_SUCCESS;
|
||||
|
||||
return PAM_AUTH_ERR;
|
||||
}
|
||||
|
||||
int pam_acct_mgmt(pam_handle_t *pamh, int flags)
|
||||
{
|
||||
(void)pamh;
|
||||
(void)flags;
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
|
||||
int pam_open_session(pam_handle_t *pamh, int flags)
|
||||
{
|
||||
(void)pamh;
|
||||
(void)flags;
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
|
||||
int pam_close_session(pam_handle_t *pamh, int flags)
|
||||
{
|
||||
(void)pamh;
|
||||
(void)flags;
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
|
||||
const char *pam_strerror(pam_handle_t *pamh, int errnum)
|
||||
{
|
||||
(void)pamh;
|
||||
switch (errnum) {
|
||||
case PAM_SUCCESS: return "Success";
|
||||
case PAM_OPEN_ERR: return "Failed to load module";
|
||||
case PAM_SYMBOL_ERR: return "Invalid symbol";
|
||||
case PAM_SERVICE_ERR: return "Error in service module";
|
||||
case PAM_SYSTEM_ERR: return "System error";
|
||||
case PAM_BUF_ERR: return "Memory buffer error";
|
||||
case PAM_PERM_DENIED: return "Permission denied";
|
||||
case PAM_AUTH_ERR: return "Authentication failure";
|
||||
case PAM_CRED_INSUFFICIENT: return "Insufficient credentials";
|
||||
case PAM_AUTHINFO_UNAVAIL: return "Authentication information unavailable";
|
||||
case PAM_USER_UNKNOWN: return "User not known to the underlying module";
|
||||
case PAM_MAXTRIES: return "Have exhausted maximum number of retries";
|
||||
case PAM_NEW_AUTHTOK_REQD: return "Authentication token is no longer valid";
|
||||
case PAM_ACCT_EXPIRED: return "Account expired";
|
||||
case PAM_SESSION_ERR: return "Cannot make/remove an entry for the session";
|
||||
case PAM_CRED_UNAVAIL: return "Authentication credentials unavailable";
|
||||
case PAM_CRED_EXPIRED: return "Authentication credentials expired";
|
||||
case PAM_CRED_ERR: return "Authentication credentials error";
|
||||
case PAM_CONV_ERR: return "Conversation error";
|
||||
case PAM_ABORT: return "Critical error - immediate abort";
|
||||
default: return "Unknown PAM error";
|
||||
}
|
||||
}
|
||||
|
||||
int pam_setcred(pam_handle_t *pamh, int flags)
|
||||
{
|
||||
(void)pamh;
|
||||
(void)flags;
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
|
||||
int pam_chauthtok(pam_handle_t *pamh, int flags)
|
||||
{
|
||||
(void)pamh;
|
||||
(void)flags;
|
||||
return PAM_AUTHTOK_ERR;
|
||||
}
|
||||
|
||||
int pam_putenv(pam_handle_t *pamh, const char *name_value)
|
||||
{
|
||||
(void)pamh;
|
||||
(void)name_value;
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
|
||||
char **pam_getenvlist(pam_handle_t *pamh)
|
||||
{
|
||||
(void)pamh;
|
||||
char **envlist = malloc(sizeof(char *));
|
||||
if (envlist) envlist[0] = NULL;
|
||||
return envlist;
|
||||
}
|
||||
|
||||
const char *pam_getenv(pam_handle_t *pamh, const char *name)
|
||||
{
|
||||
(void)pamh;
|
||||
(void)name;
|
||||
return NULL;
|
||||
}
|
||||
@@ -0,0 +1,945 @@
|
||||
//! PAM compatibility library for Red Bear OS.
|
||||
//!
|
||||
//! Implements the C PAM ABI expected by SDDM and other Linux PAM consumers,
|
||||
//! proxying authentication requests to `redbear-authd` over its Unix socket
|
||||
//! JSON protocol. All other management calls (account, session, credential,
|
||||
//! environment, data) are handled locally on the in-process `PamHandle`.
|
||||
//!
|
||||
//! The exported ABI is `extern "C"` with `#[no_mangle]` symbols. The crate
|
||||
//! is built as a `cdylib` (libpam.so) plus an `rlib` (for Rust consumers
|
||||
//! and unit tests). The C header at `include/security/pam_appl.h` declares
|
||||
//! the public interface for C consumers.
|
||||
|
||||
#![allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::{CStr, CString},
|
||||
io::{Read, Write},
|
||||
os::{
|
||||
raw::{c_char, c_int, c_void},
|
||||
unix::net::UnixStream,
|
||||
},
|
||||
ptr,
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
|
||||
use redbear_login_protocol::{AuthRequest, AuthResponse};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
const AUTHD_SOCKET_PATH: &str = "/run/redbear-authd.sock";
|
||||
const AUTHD_CONNECT_TIMEOUT_MS: u64 = 5_000;
|
||||
const AUTHD_READ_TIMEOUT_MS: u64 = 30_000;
|
||||
|
||||
// PAM return codes (mirrors <security/_pam_types.h> on Linux-PAM).
|
||||
pub const PAM_SUCCESS: c_int = 0;
|
||||
pub const PAM_OPEN_ERR: c_int = 1;
|
||||
pub const PAM_SYMBOL_ERR: c_int = 2;
|
||||
pub const PAM_SERVICE_ERR: c_int = 3;
|
||||
pub const PAM_SYSTEM_ERR: c_int = 4;
|
||||
pub const PAM_BUF_ERR: c_int = 5;
|
||||
pub const PAM_PERM_DENIED: c_int = 6;
|
||||
pub const PAM_AUTH_ERR: c_int = 7;
|
||||
pub const PAM_CRED_INSUFFICIENT: c_int = 8;
|
||||
pub const PAM_AUTHINFO_UNAVAIL: c_int = 9;
|
||||
pub const PAM_USER_UNKNOWN: c_int = 10;
|
||||
pub const PAM_MAXTRIES: c_int = 11;
|
||||
pub const PAM_NEW_AUTHTOK_REQD: c_int = 12;
|
||||
pub const PAM_ACCT_EXPIRED: c_int = 13;
|
||||
pub const PAM_SESSION_ERR: c_int = 14;
|
||||
pub const PAM_CRED_UNAVAIL: c_int = 15;
|
||||
pub const PAM_CRED_EXPIRED: c_int = 16;
|
||||
pub const PAM_CRED_ERR: c_int = 17;
|
||||
pub const PAM_NO_MODULE_DATA: c_int = 18;
|
||||
pub const PAM_CONV_ERR: c_int = 19;
|
||||
pub const PAM_AUTHTOK_ERR: c_int = 20;
|
||||
pub const PAM_AUTHTOK_RECOVER_ERR: c_int = 21;
|
||||
pub const PAM_AUTHTOK_LOCK_BUSY: c_int = 22;
|
||||
pub const PAM_AUTHTOK_DISABLE_AGING: c_int = 23;
|
||||
pub const PAM_TRY_AGAIN: c_int = 24;
|
||||
pub const PAM_IGNORE: c_int = 25;
|
||||
pub const PAM_ABORT: c_int = 26;
|
||||
pub const PAM_AUTHTOK_EXPIRED: c_int = 27;
|
||||
pub const PAM_BAD_ITEM: c_int = 29;
|
||||
pub const PAM_BAD_STATE: c_int = 30;
|
||||
pub const PAM_NO_MODULE_DATA_CRIT: c_int = 31;
|
||||
|
||||
// PAM item types.
|
||||
pub const PAM_SERVICE: c_int = 1;
|
||||
pub const PAM_USER: c_int = 2;
|
||||
pub const PAM_TTY: c_int = 3;
|
||||
pub const PAM_RHOST: c_int = 4;
|
||||
pub const PAM_CONV: c_int = 5;
|
||||
pub const PAM_AUTHTOK: c_int = 6;
|
||||
pub const PAM_OLDAUTHTOK: c_int = 7;
|
||||
pub const PAM_RUSER: c_int = 8;
|
||||
pub const PAM_USER_PROMPT: c_int = 9;
|
||||
pub const PAM_FAIL_DELAY: c_int = 10;
|
||||
pub const PAM_XDISPLAY: c_int = 11;
|
||||
pub const PAM_XAUTHDATA: c_int = 12;
|
||||
pub const PAM_AUTHTOK_TYPE: c_int = 13;
|
||||
|
||||
// PAM prompt styles.
|
||||
pub const PAM_PROMPT_ECHO_OFF: c_int = 1;
|
||||
pub const PAM_PROMPT_ECHO_ON: c_int = 2;
|
||||
pub const PAM_ERROR_MSG: c_int = 3;
|
||||
pub const PAM_TEXT_INFO: c_int = 4;
|
||||
|
||||
// PAM flag bits.
|
||||
pub const PAM_SILENT: c_int = 0x8000;
|
||||
pub const PAM_DISALLOW_NULL_AUTHTOK: c_int = 0x0001;
|
||||
pub const PAM_ESTABLISH_CRED: c_int = 0x0002;
|
||||
pub const PAM_DELETE_CRED: c_int = 0x0004;
|
||||
pub const PAM_REINITIALIZE_CRED: c_int = 0x0008;
|
||||
pub const PAM_REFRESH_CRED: c_int = 0x0010;
|
||||
pub const PAM_CHANGE_EXPIRED_AUTHTOK: c_int = 0x0020;
|
||||
|
||||
// Maximum number of messages in a single conversation round.
|
||||
pub const PAM_MAX_NUM_MSG: usize = 32;
|
||||
|
||||
// Layouts shared with C — must match `security/pam_appl.h` exactly.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct PamMessage {
|
||||
pub msg_style: c_int,
|
||||
pub msg: *const c_char,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug)]
|
||||
pub struct PamResponse {
|
||||
pub resp: *mut c_char,
|
||||
pub resp_retcode: c_int,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct PamConv {
|
||||
pub conv: Option<
|
||||
unsafe extern "C" fn(
|
||||
num_msg: c_int,
|
||||
msg: *const *const PamMessage,
|
||||
resp: *mut *mut PamResponse,
|
||||
appdata_ptr: *mut c_void,
|
||||
) -> c_int,
|
||||
>,
|
||||
pub appdata_ptr: *mut c_void,
|
||||
}
|
||||
|
||||
// Opaque handle (the public type is `pam_handle_t`).
|
||||
pub struct PamHandle {
|
||||
pub service: Option<CString>,
|
||||
pub user: Option<CString>,
|
||||
pub tty: Option<CString>,
|
||||
pub rhost: Option<CString>,
|
||||
pub ruser: Option<CString>,
|
||||
pub authtok: Option<Vec<u8>>,
|
||||
pub oldauthtok: Option<Vec<u8>>,
|
||||
pub user_prompt: Option<CString>,
|
||||
pub xdisplay: Option<CString>,
|
||||
pub authtok_type: Option<CString>,
|
||||
pub conv: Option<PamConv>,
|
||||
pub env: HashMap<String, String>,
|
||||
pub data: HashMap<String, DataEntry>,
|
||||
pub next_request_id: AtomicU64,
|
||||
}
|
||||
|
||||
pub type PamDataCleanup =
|
||||
Option<unsafe extern "C" fn(pamh: *mut PamHandle, data: *mut c_void, error_status: c_int)>;
|
||||
|
||||
pub struct DataEntry {
|
||||
pub data: *mut c_void,
|
||||
pub cleanup: PamDataCleanup,
|
||||
}
|
||||
|
||||
impl PamHandle {
|
||||
fn new(service: Option<&str>, user: Option<&str>, conv: Option<PamConv>) -> Self {
|
||||
Self {
|
||||
service: service.map(|s| CString::new(s).unwrap_or_default()),
|
||||
user: user.map(|s| CString::new(s).unwrap_or_default()),
|
||||
tty: None,
|
||||
rhost: None,
|
||||
ruser: None,
|
||||
authtok: None,
|
||||
oldauthtok: None,
|
||||
user_prompt: None,
|
||||
xdisplay: None,
|
||||
authtok_type: None,
|
||||
conv,
|
||||
env: HashMap::new(),
|
||||
data: HashMap::new(),
|
||||
next_request_id: AtomicU64::new(1),
|
||||
}
|
||||
}
|
||||
|
||||
fn next_request_id(&self) -> u64 {
|
||||
self.next_request_id.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_cstr_arg<'a>(ptr: *const c_char) -> Option<&'a str> {
|
||||
if ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
unsafe { CStr::from_ptr(ptr) }.to_str().ok()
|
||||
}
|
||||
|
||||
fn clone_cstr_arg(ptr: *const c_char) -> Option<CString> {
|
||||
read_cstr_arg(ptr).map(|s| CString::new(s).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn read_const_data<'a>(ptr: *const c_void, len: usize) -> Option<Vec<u8>> {
|
||||
if ptr.is_null() || len == 0 {
|
||||
return None;
|
||||
}
|
||||
let slice = unsafe { std::slice::from_raw_parts(ptr as *const u8, len) };
|
||||
Some(slice.to_vec())
|
||||
}
|
||||
|
||||
fn error_string(errnum: c_int) -> &'static str {
|
||||
match errnum {
|
||||
PAM_SUCCESS => "Success",
|
||||
PAM_OPEN_ERR => "Failed to load module",
|
||||
PAM_SYMBOL_ERR => "Invalid symbol",
|
||||
PAM_SERVICE_ERR => "Error in service module",
|
||||
PAM_SYSTEM_ERR => "System error",
|
||||
PAM_BUF_ERR => "Memory buffer error",
|
||||
PAM_PERM_DENIED => "Permission denied",
|
||||
PAM_AUTH_ERR => "Authentication failure",
|
||||
PAM_CRED_INSUFFICIENT => "Insufficient credentials",
|
||||
PAM_AUTHINFO_UNAVAIL => "Authentication information is unavailable",
|
||||
PAM_USER_UNKNOWN => "User is not known to the underlying authentication module",
|
||||
PAM_MAXTRIES => "Have exhausted maximum number of retries for service",
|
||||
PAM_NEW_AUTHTOK_REQD => "Authentication token is no longer valid; new one required",
|
||||
PAM_ACCT_EXPIRED => "User account has expired",
|
||||
PAM_SESSION_ERR => "Cannot make/remove an entry for the session",
|
||||
PAM_CRED_UNAVAIL => "Authentication credentials are unavailable",
|
||||
PAM_CRED_EXPIRED => "Authentication credentials have expired",
|
||||
PAM_CRED_ERR => "Failure setting user credentials",
|
||||
PAM_NO_MODULE_DATA => "No module-specific data is present",
|
||||
PAM_CONV_ERR => "Conversation error",
|
||||
PAM_AUTHTOK_ERR => "Authentication token manipulation error",
|
||||
PAM_AUTHTOK_RECOVER_ERR => "Authentication token cannot be recovered",
|
||||
PAM_AUTHTOK_LOCK_BUSY => "Authentication token lock busy",
|
||||
PAM_AUTHTOK_DISABLE_AGING => "Authentication token aging disabled",
|
||||
PAM_TRY_AGAIN => "Failed preliminary check by password service",
|
||||
PAM_IGNORE => "Please ignore underlying account module",
|
||||
PAM_ABORT => "Critical error; abort immediately",
|
||||
PAM_AUTHTOK_EXPIRED => "Authentication token has expired",
|
||||
PAM_BAD_ITEM => "The application passed an invalid item to the PAM library",
|
||||
PAM_BAD_STATE => "The PAM library is in a bad state",
|
||||
_ => "Unknown PAM error",
|
||||
}
|
||||
}
|
||||
|
||||
fn authenticate_with_authd(handle: &PamHandle, username: &str, password: &[u8], vt: u32) -> c_int {
|
||||
let mut stream = match UnixStream::connect(AUTHD_SOCKET_PATH) {
|
||||
Ok(stream) => stream,
|
||||
Err(_) => return PAM_AUTHINFO_UNAVAIL,
|
||||
};
|
||||
|
||||
let _ = stream.set_read_timeout(Some(std::time::Duration::from_millis(AUTHD_READ_TIMEOUT_MS)));
|
||||
let _ = stream.set_write_timeout(Some(std::time::Duration::from_millis(AUTHD_CONNECT_TIMEOUT_MS)));
|
||||
|
||||
let request_id = handle.next_request_id();
|
||||
let password_str = match std::str::from_utf8(password) {
|
||||
Ok(s) => s.to_owned(),
|
||||
Err(_) => return PAM_AUTH_ERR,
|
||||
};
|
||||
let request = AuthRequest::Authenticate {
|
||||
request_id,
|
||||
username: username.to_owned(),
|
||||
password: password_str,
|
||||
vt,
|
||||
};
|
||||
|
||||
let payload = match serde_json::to_string(&request) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return PAM_AUTH_ERR,
|
||||
};
|
||||
|
||||
if stream.write_all(payload.as_bytes()).is_err() {
|
||||
return PAM_AUTH_ERR;
|
||||
}
|
||||
if stream.write_all(b"\n").is_err() {
|
||||
return PAM_AUTH_ERR;
|
||||
}
|
||||
|
||||
let mut line = String::new();
|
||||
let mut limited = (&mut stream).take(1_048_576);
|
||||
if limited.read_to_string(&mut line).is_err() {
|
||||
return PAM_AUTH_ERR;
|
||||
}
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
return PAM_AUTH_ERR;
|
||||
}
|
||||
|
||||
match serde_json::from_str::<AuthResponse>(line) {
|
||||
Ok(AuthResponse::AuthenticateResult { ok: true, .. }) => PAM_SUCCESS,
|
||||
Ok(AuthResponse::AuthenticateResult { ok: false, .. }) => PAM_AUTH_ERR,
|
||||
Ok(AuthResponse::Error { .. }) => PAM_AUTHINFO_UNAVAIL,
|
||||
Ok(_) => PAM_AUTH_ERR,
|
||||
Err(_) => {
|
||||
// Tolerate older / partial JSON envelopes: accept top-level `ok` boolean.
|
||||
match serde_json::from_str::<JsonValue>(line) {
|
||||
Ok(JsonValue::Object(map)) => match map.get("ok") {
|
||||
Some(JsonValue::Bool(true)) => PAM_SUCCESS,
|
||||
Some(JsonValue::Bool(false)) => PAM_AUTH_ERR,
|
||||
_ => PAM_AUTH_ERR,
|
||||
},
|
||||
_ => PAM_AUTH_ERR,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_conversation(conv: &PamConv, messages: &[PamMessage]) -> Result<Vec<Option<Vec<u8>>>, c_int> {
|
||||
let cb = match conv.conv {
|
||||
Some(cb) => cb,
|
||||
None => return Err(PAM_CONV_ERR),
|
||||
};
|
||||
|
||||
if messages.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if messages.len() > PAM_MAX_NUM_MSG {
|
||||
return Err(PAM_CONV_ERR);
|
||||
}
|
||||
|
||||
let mut msg_array: Vec<*const PamMessage> = messages.iter().map(|m| m as *const PamMessage).collect();
|
||||
let mut resp_ptr: *mut PamResponse = ptr::null_mut();
|
||||
|
||||
let rc = unsafe {
|
||||
cb(
|
||||
messages.len() as c_int,
|
||||
msg_array.as_mut_ptr(),
|
||||
&mut resp_ptr,
|
||||
conv.appdata_ptr,
|
||||
)
|
||||
};
|
||||
msg_array.clear();
|
||||
if rc != PAM_SUCCESS {
|
||||
return Err(rc);
|
||||
}
|
||||
if resp_ptr.is_null() {
|
||||
return Err(PAM_CONV_ERR);
|
||||
}
|
||||
|
||||
let responses = unsafe { std::slice::from_raw_parts(resp_ptr, messages.len()) };
|
||||
let mut out: Vec<Option<Vec<u8>>> = Vec::with_capacity(messages.len());
|
||||
for r in responses {
|
||||
if r.resp.is_null() {
|
||||
out.push(None);
|
||||
} else {
|
||||
let cstr = unsafe { CStr::from_ptr(r.resp) };
|
||||
let mut bytes = cstr.to_bytes().to_vec();
|
||||
out.push(Some(std::mem::take(&mut bytes)));
|
||||
}
|
||||
}
|
||||
|
||||
// Free the response array and zero the C-side buffers before returning.
|
||||
for r in responses {
|
||||
if !r.resp.is_null() {
|
||||
unsafe {
|
||||
ptr::write_bytes(r.resp as *mut u8, 0, libc_compat_strlen(r.resp));
|
||||
let _ = CString::from_raw(r.resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe {
|
||||
let _ = Box::from_raw(std::slice::from_raw_parts_mut(resp_ptr, messages.len()));
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn libc_compat_strlen(ptr: *const c_char) -> usize {
|
||||
let mut len = 0;
|
||||
unsafe {
|
||||
while *ptr.add(len) != 0 {
|
||||
len += 1;
|
||||
}
|
||||
}
|
||||
len
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn libc_compat_strlen(ptr: *const c_char) -> usize {
|
||||
let mut len = 0;
|
||||
unsafe {
|
||||
while *ptr.add(len) != 0 {
|
||||
len += 1;
|
||||
}
|
||||
}
|
||||
len
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_start(
|
||||
service_name: *const c_char,
|
||||
user: *const c_char,
|
||||
conv: *const PamConv,
|
||||
pamh: *mut *mut PamHandle,
|
||||
) -> c_int {
|
||||
if pamh.is_null() {
|
||||
return PAM_SYSTEM_ERR;
|
||||
}
|
||||
|
||||
let service = read_cstr_arg(service_name);
|
||||
let user = read_cstr_arg(user);
|
||||
let conv = if conv.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(unsafe { *conv })
|
||||
};
|
||||
|
||||
let handle = Box::new(PamHandle::new(service, user, conv));
|
||||
unsafe { *pamh = Box::into_raw(handle) };
|
||||
PAM_SUCCESS
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_end(pamh: *mut PamHandle, pam_status: c_int) -> c_int {
|
||||
if pamh.is_null() {
|
||||
return PAM_SYSTEM_ERR;
|
||||
}
|
||||
let _ = pam_status;
|
||||
|
||||
let handle = unsafe { Box::from_raw(pamh) };
|
||||
|
||||
// Run data cleanup callbacks before dropping the handle storage.
|
||||
for (_, entry) in handle.data.into_iter() {
|
||||
if let Some(cb) = entry.cleanup {
|
||||
unsafe { cb(pamh, entry.data, pam_status) };
|
||||
}
|
||||
}
|
||||
|
||||
PAM_SUCCESS
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_set_item(pamh: *mut PamHandle, item_type: c_int, item: *const c_void) -> c_int {
|
||||
if pamh.is_null() {
|
||||
return PAM_SYSTEM_ERR;
|
||||
}
|
||||
let handle = unsafe { &mut *pamh };
|
||||
|
||||
match item_type {
|
||||
PAM_SERVICE => handle.service = clone_cstr_arg(item as *const c_char),
|
||||
PAM_USER => handle.user = clone_cstr_arg(item as *const c_char),
|
||||
PAM_TTY => handle.tty = clone_cstr_arg(item as *const c_char),
|
||||
PAM_RHOST => handle.rhost = clone_cstr_arg(item as *const c_char),
|
||||
PAM_RUSER => handle.ruser = clone_cstr_arg(item as *const c_char),
|
||||
PAM_USER_PROMPT => handle.user_prompt = clone_cstr_arg(item as *const c_char),
|
||||
PAM_XDISPLAY => handle.xdisplay = clone_cstr_arg(item as *const c_char),
|
||||
PAM_AUTHTOK_TYPE => handle.authtok_type = clone_cstr_arg(item as *const c_char),
|
||||
PAM_AUTHTOK => {
|
||||
handle.authtok = read_const_data(item, item_len_from_cstring(item as *const c_char))
|
||||
}
|
||||
PAM_OLDAUTHTOK => {
|
||||
handle.oldauthtok = read_const_data(item, item_len_from_cstring(item as *const c_char))
|
||||
}
|
||||
PAM_CONV => {
|
||||
if !item.is_null() {
|
||||
handle.conv = Some(unsafe { *(item as *const PamConv) });
|
||||
} else {
|
||||
handle.conv = None;
|
||||
}
|
||||
}
|
||||
PAM_FAIL_DELAY | PAM_XAUTHDATA => return PAM_BAD_ITEM,
|
||||
_ => return PAM_BAD_ITEM,
|
||||
}
|
||||
PAM_SUCCESS
|
||||
}
|
||||
|
||||
fn item_len_from_cstring(ptr: *const c_char) -> usize {
|
||||
if ptr.is_null() {
|
||||
return 0;
|
||||
}
|
||||
unsafe { CStr::from_ptr(ptr) }.to_bytes().len()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_get_item(pamh: *const PamHandle, item_type: c_int, item: *mut *const c_void) -> c_int {
|
||||
if pamh.is_null() || item.is_null() {
|
||||
return PAM_SYSTEM_ERR;
|
||||
}
|
||||
let handle = unsafe { &*pamh };
|
||||
|
||||
let value: *const c_void = match item_type {
|
||||
PAM_SERVICE => handle.service.as_ref().map_or(ptr::null(), |s| s.as_ptr()) as *const c_void,
|
||||
PAM_USER => handle.user.as_ref().map_or(ptr::null(), |s| s.as_ptr()) as *const c_void,
|
||||
PAM_TTY => handle.tty.as_ref().map_or(ptr::null(), |s| s.as_ptr()) as *const c_void,
|
||||
PAM_RHOST => handle.rhost.as_ref().map_or(ptr::null(), |s| s.as_ptr()) as *const c_void,
|
||||
PAM_RUSER => handle.ruser.as_ref().map_or(ptr::null(), |s| s.as_ptr()) as *const c_void,
|
||||
PAM_USER_PROMPT => handle.user_prompt.as_ref().map_or(ptr::null(), |s| s.as_ptr()) as *const c_void,
|
||||
PAM_XDISPLAY => handle.xdisplay.as_ref().map_or(ptr::null(), |s| s.as_ptr()) as *const c_void,
|
||||
PAM_AUTHTOK_TYPE => handle.authtok_type.as_ref().map_or(ptr::null(), |s| s.as_ptr()) as *const c_void,
|
||||
PAM_AUTHTOK => handle.authtok.as_ref().map_or(ptr::null(), |v| v.as_ptr()) as *const c_void,
|
||||
PAM_OLDAUTHTOK => handle.oldauthtok.as_ref().map_or(ptr::null(), |v| v.as_ptr()) as *const c_void,
|
||||
PAM_CONV => {
|
||||
if let Some(conv) = handle.conv {
|
||||
&conv as *const PamConv as *const c_void
|
||||
} else {
|
||||
ptr::null()
|
||||
}
|
||||
}
|
||||
PAM_FAIL_DELAY | PAM_XAUTHDATA => {
|
||||
unsafe { *item = ptr::null() };
|
||||
return PAM_BAD_ITEM;
|
||||
}
|
||||
_ => {
|
||||
unsafe { *item = ptr::null() };
|
||||
return PAM_BAD_ITEM;
|
||||
}
|
||||
};
|
||||
|
||||
unsafe { *item = value };
|
||||
PAM_SUCCESS
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_get_data(
|
||||
pamh: *const PamHandle,
|
||||
module_data_name: *const c_char,
|
||||
data: *mut *mut c_void,
|
||||
) -> c_int {
|
||||
if pamh.is_null() || module_data_name.is_null() || data.is_null() {
|
||||
return PAM_SYSTEM_ERR;
|
||||
}
|
||||
let handle = unsafe { &*pamh };
|
||||
let name = match read_cstr_arg(module_data_name) {
|
||||
Some(name) => name,
|
||||
None => return PAM_SYSTEM_ERR,
|
||||
};
|
||||
|
||||
match handle.data.get(name) {
|
||||
Some(entry) => {
|
||||
unsafe { *data = entry.data };
|
||||
PAM_SUCCESS
|
||||
}
|
||||
None => PAM_NO_MODULE_DATA,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_set_data(
|
||||
pamh: *mut PamHandle,
|
||||
module_data_name: *const c_char,
|
||||
data: *mut c_void,
|
||||
cleanup: PamDataCleanup,
|
||||
) -> c_int {
|
||||
if pamh.is_null() || module_data_name.is_null() {
|
||||
return PAM_SYSTEM_ERR;
|
||||
}
|
||||
let handle = unsafe { &mut *pamh };
|
||||
let name = match read_cstr_arg(module_data_name) {
|
||||
Some(name) => name.to_owned(),
|
||||
None => return PAM_SYSTEM_ERR,
|
||||
};
|
||||
|
||||
// If we are replacing an existing entry, run its cleanup first.
|
||||
if let Some(previous) = handle.data.remove(&name) {
|
||||
if let Some(cb) = previous.cleanup {
|
||||
unsafe { cb(pamh, previous.data, 0) };
|
||||
}
|
||||
}
|
||||
|
||||
handle.data.insert(
|
||||
name,
|
||||
DataEntry {
|
||||
data,
|
||||
cleanup,
|
||||
},
|
||||
);
|
||||
PAM_SUCCESS
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_authenticate(pamh: *mut PamHandle, flags: c_int) -> c_int {
|
||||
if pamh.is_null() {
|
||||
return PAM_SYSTEM_ERR;
|
||||
}
|
||||
let _ = flags;
|
||||
|
||||
// SAFETY: pamh is checked for null and only read here; we never hand it
|
||||
// out concurrently to other threads. The handle outlives this call.
|
||||
let handle = unsafe { &*pamh };
|
||||
|
||||
// Refuse authentication when no username is available — this matches
|
||||
// Linux-PAM's behavior of mapping "no user" to PAM_USER_UNKNOWN rather
|
||||
// than a generic auth failure.
|
||||
if handle.user.is_none() {
|
||||
return PAM_USER_UNKNOWN;
|
||||
}
|
||||
|
||||
// If a conv is set, prompt for the password first.
|
||||
let password = if handle.conv.is_some() && handle.authtok.is_none() {
|
||||
let prompt = CString::new("Password: ").unwrap_or_default();
|
||||
let prompt_msg = PamMessage {
|
||||
msg_style: PAM_PROMPT_ECHO_OFF,
|
||||
msg: prompt.as_ptr(),
|
||||
};
|
||||
match run_conversation(handle.conv.as_ref().unwrap(), &[prompt_msg]) {
|
||||
Ok(resp) if resp.len() == 1 => match resp.into_iter().next() {
|
||||
Some(Some(bytes)) => bytes,
|
||||
_ => return PAM_AUTH_ERR,
|
||||
},
|
||||
_ => return PAM_CONV_ERR,
|
||||
}
|
||||
} else {
|
||||
match handle.authtok.clone() {
|
||||
Some(bytes) => bytes,
|
||||
None => return PAM_AUTH_ERR,
|
||||
}
|
||||
};
|
||||
|
||||
let username = match handle.user.as_ref() {
|
||||
Some(u) => match u.to_str() {
|
||||
Ok(s) => s.to_owned(),
|
||||
Err(_) => return PAM_USER_UNKNOWN,
|
||||
},
|
||||
None => return PAM_USER_UNKNOWN,
|
||||
};
|
||||
|
||||
authenticate_with_authd(handle, &username, &password, 0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_acct_mgmt(pamh: *mut PamHandle, flags: c_int) -> c_int {
|
||||
if pamh.is_null() {
|
||||
return PAM_SYSTEM_ERR;
|
||||
}
|
||||
let _ = flags;
|
||||
// redbear-authd owns account validity (uid/shell policy). If we ever
|
||||
// need finer checks, query redbear-authd's account request here. For
|
||||
// now, accept the account.
|
||||
PAM_SUCCESS
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_open_session(pamh: *mut PamHandle, flags: c_int) -> c_int {
|
||||
if pamh.is_null() {
|
||||
return PAM_SYSTEM_ERR;
|
||||
}
|
||||
let _ = flags;
|
||||
PAM_SUCCESS
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_close_session(pamh: *mut PamHandle, flags: c_int) -> c_int {
|
||||
if pamh.is_null() {
|
||||
return PAM_SYSTEM_ERR;
|
||||
}
|
||||
let _ = flags;
|
||||
PAM_SUCCESS
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_setcred(pamh: *mut PamHandle, flags: c_int) -> c_int {
|
||||
if pamh.is_null() {
|
||||
return PAM_SYSTEM_ERR;
|
||||
}
|
||||
let _ = flags;
|
||||
PAM_SUCCESS
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_chauthtok(pamh: *mut PamHandle, flags: c_int) -> c_int {
|
||||
if pamh.is_null() {
|
||||
return PAM_SYSTEM_ERR;
|
||||
}
|
||||
let _ = flags;
|
||||
// redbear-authd's current protocol surface is authenticate/session/power.
|
||||
// Token rotation requires an extension to the authd protocol; surface the
|
||||
// missing capability honestly rather than claiming success.
|
||||
PAM_AUTHTOK_ERR
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_strerror(pamh: *mut PamHandle, errnum: c_int) -> *const c_char {
|
||||
let _ = pamh;
|
||||
// For the error string we need a stable pointer per-thread; the simplest
|
||||
// portable approach is a thread-local rotating buffer. The pointer is
|
||||
// stable only for the duration of the call (the caller is expected to
|
||||
// copy it if it needs persistence).
|
||||
thread_local! {
|
||||
static BUFFER: std::cell::RefCell<[u8; 256]> = const { std::cell::RefCell::new([0u8; 256]) };
|
||||
}
|
||||
let msg = error_string(errnum);
|
||||
BUFFER.with(|cell| {
|
||||
let mut buf = cell.borrow_mut();
|
||||
let copy_len = std::cmp::min(msg.len(), buf.len() - 1);
|
||||
buf[..copy_len].copy_from_slice(&msg.as_bytes()[..copy_len]);
|
||||
buf[copy_len] = 0;
|
||||
buf.as_ptr() as *const c_char
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_putenv(pamh: *mut PamHandle, name_value: *const c_char) -> c_int {
|
||||
if pamh.is_null() || name_value.is_null() {
|
||||
return PAM_SYSTEM_ERR;
|
||||
}
|
||||
let handle = unsafe { &mut *pamh };
|
||||
let raw = unsafe { CStr::from_ptr(name_value) };
|
||||
let s = match raw.to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return PAM_SYSTEM_ERR,
|
||||
};
|
||||
|
||||
if let Some((name, value)) = s.split_once('=') {
|
||||
if name.is_empty() {
|
||||
return PAM_SYSTEM_ERR;
|
||||
}
|
||||
handle.env.insert(name.to_owned(), value.to_owned());
|
||||
PAM_SUCCESS
|
||||
} else {
|
||||
// `pam_putenv(pamh, "NAME")` removes the variable.
|
||||
handle.env.remove(s);
|
||||
PAM_SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_getenv(pamh: *mut PamHandle, name: *const c_char) -> *const c_char {
|
||||
if pamh.is_null() || name.is_null() {
|
||||
return ptr::null();
|
||||
}
|
||||
let handle = unsafe { &*pamh };
|
||||
let key = match read_cstr_arg(name) {
|
||||
Some(s) => s,
|
||||
None => return ptr::null(),
|
||||
};
|
||||
match handle.env.get(key) {
|
||||
Some(v) => {
|
||||
// The pointer is leaked: the env entries are kept alive by
|
||||
// `handle.env`, so the pointer remains valid until pam_end.
|
||||
v.as_ptr() as *const c_char
|
||||
}
|
||||
None => ptr::null(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_getenvlist(pamh: *mut PamHandle) -> *mut *mut c_char {
|
||||
if pamh.is_null() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let handle = unsafe { &*pamh };
|
||||
|
||||
// Layout: array of (n+1) char* pointers, last is NULL. The strings are
|
||||
// appended after the pointer array and live for the lifetime of the
|
||||
// handle (which is the lifetime expected by Linux-PAM consumers).
|
||||
let mut entries: Vec<CString> = handle
|
||||
.env
|
||||
.iter()
|
||||
.map(|(k, v)| CString::new(format!("{k}={v}")).unwrap_or_default())
|
||||
.collect();
|
||||
|
||||
let total = entries.len() + 1;
|
||||
let layout_size = total * std::mem::size_of::<*mut c_char>();
|
||||
let strings_size: usize = entries.iter().map(|s| s.as_bytes_with_nul().len()).sum();
|
||||
let bytes = layout_size + strings_size;
|
||||
|
||||
unsafe {
|
||||
let raw = libc_compat_alloc(bytes) as *mut u8;
|
||||
if raw.is_null() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
ptr::write_bytes(raw, 0, bytes);
|
||||
let ptrs = raw as *mut *mut c_char;
|
||||
let mut cursor = raw.add(layout_size);
|
||||
for (i, s) in entries.iter_mut().enumerate() {
|
||||
let bytes = s.as_bytes_with_nul();
|
||||
ptr::copy_nonoverlapping(bytes.as_ptr(), cursor, bytes.len());
|
||||
*ptrs.add(i) = cursor as *mut c_char;
|
||||
cursor = cursor.add(bytes.len());
|
||||
}
|
||||
*ptrs.add(entries.len()) = ptr::null_mut();
|
||||
// Leak the CStrings so the pointers stay valid.
|
||||
let _: Box<[CString]> = entries.into_boxed_slice();
|
||||
ptrs
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
unsafe fn libc_compat_alloc(size: usize) -> *mut c_void {
|
||||
extern "C" {
|
||||
fn malloc(size: usize) -> *mut c_void;
|
||||
}
|
||||
malloc(size)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
unsafe fn libc_compat_alloc(size: usize) -> *mut c_void {
|
||||
extern "C" {
|
||||
fn malloc(size: usize) -> *mut c_void;
|
||||
}
|
||||
malloc(size)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::ffi::CString;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
fn make_handle() -> *mut PamHandle {
|
||||
let service = CString::new("sddm").unwrap();
|
||||
let user = CString::new("root").unwrap();
|
||||
let mut pamh: *mut PamHandle = ptr::null_mut();
|
||||
let rc = pam_start(service.as_ptr(), user.as_ptr(), ptr::null(), &mut pamh);
|
||||
assert_eq!(rc, PAM_SUCCESS);
|
||||
assert!(!pamh.is_null());
|
||||
pamh
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_and_end_lifecycle() {
|
||||
let pamh = make_handle();
|
||||
assert_eq!(pam_end(pamh, PAM_SUCCESS), PAM_SUCCESS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_and_get_string_items() {
|
||||
let pamh = make_handle();
|
||||
let tty = CString::new("/dev/tty1").unwrap();
|
||||
assert_eq!(pam_set_item(pamh, PAM_TTY, tty.as_ptr() as *const c_void), PAM_SUCCESS);
|
||||
|
||||
let mut item: *const c_void = ptr::null();
|
||||
assert_eq!(pam_get_item(pamh, PAM_TTY, &mut item), PAM_SUCCESS);
|
||||
let round = unsafe { CStr::from_ptr(item as *const c_char) };
|
||||
assert_eq!(round.to_str().unwrap(), "/dev/tty1");
|
||||
assert_eq!(pam_end(pamh, PAM_SUCCESS), PAM_SUCCESS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_item_returns_bad_item() {
|
||||
let pamh = make_handle();
|
||||
let mut item: *const c_void = ptr::null();
|
||||
assert_eq!(pam_get_item(pamh, PAM_FAIL_DELAY, &mut item), PAM_BAD_ITEM);
|
||||
assert_eq!(pam_end(pamh, PAM_SUCCESS), PAM_SUCCESS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_set_get_with_cleanup() {
|
||||
static CLEANUP_COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
||||
unsafe extern "C" fn cleanup(
|
||||
_pamh: *mut PamHandle,
|
||||
_data: *mut c_void,
|
||||
_error_status: c_int,
|
||||
) {
|
||||
CLEANUP_COUNT.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
|
||||
let pamh = make_handle();
|
||||
let key = CString::new("module_data").unwrap();
|
||||
let sentinel: c_int = 0xC0FFEE;
|
||||
let rc = pam_set_data(
|
||||
pamh,
|
||||
key.as_ptr(),
|
||||
&sentinel as *const c_int as *mut c_void,
|
||||
Some(cleanup),
|
||||
);
|
||||
assert_eq!(rc, PAM_SUCCESS);
|
||||
|
||||
let mut out: *mut c_void = ptr::null_mut();
|
||||
assert_eq!(pam_get_data(pamh, key.as_ptr(), &mut out), PAM_SUCCESS);
|
||||
assert_eq!(out as usize, &sentinel as *const c_int as usize);
|
||||
|
||||
// Missing key returns NO_MODULE_DATA.
|
||||
let missing = CString::new("other").unwrap();
|
||||
let mut dummy: *mut c_void = ptr::null_mut();
|
||||
assert_eq!(pam_get_data(pamh, missing.as_ptr(), &mut dummy), PAM_NO_MODULE_DATA);
|
||||
|
||||
assert_eq!(pam_end(pamh, PAM_SUCCESS), PAM_SUCCESS);
|
||||
assert!(CLEANUP_COUNT.load(std::sync::atomic::Ordering::SeqCst) >= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_round_trip() {
|
||||
let pamh = make_handle();
|
||||
let entry = CString::new("XDG_SESSION_TYPE=wayland").unwrap();
|
||||
assert_eq!(pam_putenv(pamh, entry.as_ptr()), PAM_SUCCESS);
|
||||
|
||||
let key = CString::new("XDG_SESSION_TYPE").unwrap();
|
||||
let raw = pam_getenv(pamh, key.as_ptr());
|
||||
assert!(!raw.is_null());
|
||||
assert_eq!(unsafe { CStr::from_ptr(raw) }.to_str().unwrap(), "wayland");
|
||||
|
||||
// pam_getenvlist must produce at least one entry plus a NULL terminator.
|
||||
let list = pam_getenvlist(pamh);
|
||||
assert!(!list.is_null());
|
||||
unsafe {
|
||||
assert!(!(*list).is_null());
|
||||
assert_eq!(CStr::from_ptr(*list).to_str().unwrap(), "XDG_SESSION_TYPE=wayland");
|
||||
}
|
||||
assert_eq!(pam_end(pamh, PAM_SUCCESS), PAM_SUCCESS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_remove_via_putenv() {
|
||||
let pamh = make_handle();
|
||||
let entry = CString::new("FOO=1").unwrap();
|
||||
assert_eq!(pam_putenv(pamh, entry.as_ptr()), PAM_SUCCESS);
|
||||
let key = CString::new("FOO").unwrap();
|
||||
assert!(!pam_getenv(pamh, key.as_ptr()).is_null());
|
||||
let remove = CString::new("FOO").unwrap();
|
||||
assert_eq!(pam_putenv(pamh, remove.as_ptr()), PAM_SUCCESS);
|
||||
assert!(pam_getenv(pamh, key.as_ptr()).is_null());
|
||||
assert_eq!(pam_end(pamh, PAM_SUCCESS), PAM_SUCCESS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chauthtok_returns_authtok_err() {
|
||||
let pamh = make_handle();
|
||||
// The current authd protocol does not include password change; the
|
||||
// operation honestly reports that to the caller.
|
||||
assert_eq!(pam_chauthtok(pamh, 0), PAM_AUTHTOK_ERR);
|
||||
assert_eq!(pam_end(pamh, PAM_SUCCESS), PAM_SUCCESS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strerror_returns_non_null_strings() {
|
||||
for code in [PAM_SUCCESS, PAM_AUTH_ERR, PAM_USER_UNKNOWN, PAM_BUF_ERR, 9999] {
|
||||
let ptr = pam_strerror(ptr::null_mut(), code);
|
||||
assert!(!ptr.is_null());
|
||||
let text = unsafe { CStr::from_ptr(ptr) }.to_str().unwrap();
|
||||
assert!(!text.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticate_without_user_returns_user_unknown() {
|
||||
let service = CString::new("sddm").unwrap();
|
||||
let mut pamh: *mut PamHandle = ptr::null_mut();
|
||||
let rc = pam_start(service.as_ptr(), ptr::null(), ptr::null(), &mut pamh);
|
||||
assert_eq!(rc, PAM_SUCCESS);
|
||||
// No user set, no conv -> authentication cannot proceed.
|
||||
assert_eq!(pam_authenticate(pamh, 0), PAM_USER_UNKNOWN);
|
||||
assert_eq!(pam_end(pamh, PAM_SUCCESS), PAM_SUCCESS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticate_with_user_but_no_authd_socket_returns_auth_err() {
|
||||
// redbear-authd is not running on the host, so the socket connect
|
||||
// fails and the result is mapped to PAM_AUTH_ERR.
|
||||
let pamh = make_handle();
|
||||
let rc = pam_authenticate(pamh, 0);
|
||||
assert!(rc == PAM_AUTH_ERR || rc == PAM_AUTHINFO_UNAVAIL);
|
||||
assert_eq!(pam_end(pamh, PAM_SUCCESS), PAM_SUCCESS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acct_session_cred_are_successful() {
|
||||
let pamh = make_handle();
|
||||
assert_eq!(pam_acct_mgmt(pamh, 0), PAM_SUCCESS);
|
||||
assert_eq!(pam_open_session(pamh, 0), PAM_SUCCESS);
|
||||
assert_eq!(pam_close_session(pamh, 0), PAM_SUCCESS);
|
||||
assert_eq!(pam_setcred(pamh, PAM_ESTABLISH_CRED), PAM_SUCCESS);
|
||||
assert_eq!(pam_end(pamh, PAM_SUCCESS), PAM_SUCCESS);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user