altprivsep.c revision 7c478bd95313f5f23a4c958a745db2134aa03244
/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
* or http://www.opensolaris.org/os/licensing.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at usr/src/OPENSOLARIS.LICENSE.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information: Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
* Copyright 2005 Sun Microsystems, Inc. All rights reserved.
* Use is subject to license terms.
*/
#pragma ident "%Z%%M% %I% %E% SMI"
#include "includes.h"
#include "atomicio.h"
#include "auth.h"
#include "bufaux.h"
#include "buffer.h"
#include "cipher.h"
#include "compat.h"
#include "dispatch.h"
#include "getput.h"
#include "kex.h"
#include "log.h"
#include "mac.h"
#include "packet.h"
#include "uidswap.h"
#include "ssh2.h"
#include "sshlogin.h"
#include "xmalloc.h"
#include "altprivsep.h"
#include <fcntl.h>
#include <sys/types.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
extern Kex *xxx_kex;
static Buffer to_monitor;
static Buffer from_monitor;
/*
* Sun's Alternative Privilege Separation basics:
*
* Abstract
* --------
*
* sshd(1M) fork()s and drops privs in the child while retaining privs
* in the parent (a.k.a., the monitor). The unprivileged sshd and the
* monitor talk over a pipe using a simple protocol.
*
* The monitor protocol is all about having the monitor carry out the
* only operations that require privileges OR access to privileged
* resources. These are: utmpx/wtmpx record keeping, auditing, and
* SSHv2 re-keying.
*
* Re-Keying
* ---------
*
* Re-keying is the only protocol version specific aspect of sshd in
* which the monitor gets involved.
*
* The monitor processes all SSHv2 re-key protocol packets, but the
* unprivileged sshd process does the transport layer crypto for those
* packets.
*
* The monitor and its unprivileged sshd child process treat
* SSH_MSG_NEWKEYS SSH2 messages specially: a) the monitor does not call
* set_newkeys(), but b) the child asks the monitor for the set of
* negotiated algorithms, key, IV and what not for the relevant
* transport direction and then calls set_newkeys().
*
* Monitor Protocol
* ----------------
*
* Monitor IPC message formats are similar to SSHv2 messages, minus
* compression, encryption, padding and MACs:
*
* - 4 octet message length
* - message data
* - 1 octet message type
* - message data
*
* In broad strokes:
*
* - IPC: pipe, exit(2)/wait4(2)
*
* - threads: the monitor and child are single-threaded
*
* - monitor main loop: a variant of server_loop2(), for re-keying only
* - unpriv child main loop: server_loop2(), as usual
*
* - protocol:
* - key exchange packets are always forwarded as is to the monitor
* - newkeys, record_login(), record_logout() are special packets
* using the packet type range reserved for local extensions
*
* - the child drops privs and runs like a normal sshd, except that it
* sets dispatch handlers for key exchange packets that forward the
* packets to the monitor
*
* Event loops:
*
* - all monitor protocols are synchronous: because the SSHv2 rekey
* protocols are synchronous and because the other monitor operations
* are synchronous (or have no replies),
*
* - server_loop2() is modified to check the monitor pipe for rekey
* packets to forward to the client
*
* - and dispatch handlers are set, upon receipt of KEXINIT (and reset
* when NEWKEYS is sent out) to forward incoming rekey packets to the
* monitor.
*
* - the monitor runs an event loop not unlike server_loop2() and runs
* key exchanges almost exactly as a pre-altprivsep sshd would
*
* - unpriv sshd exit -> monitor cleanup (including audit logout) and exit
*
* - fatal() in monitor -> forcibly shutdown() socket and kill/wait for
* child (so that the audit event for the logout better reflects
* reality -- i.e., logged out means logged out, but for bg jobs)
*
* Message formats:
*
* - key exchange packets/replies forwarded "as is"
*
* - all other monitor requests are sent as SSH2_PRIV_MSG_ALTPRIVSEP and have a
* sub-type identifier (one octet)
* - private request sub-types include:
* - get new shared secret from last re-key
* - record login (utmpx/wtmpx), request data contains three arguments:
* pid, ttyname, program name
* - record logout (utmpx/wtmpx), request data contains one argument: pid
*
* Reply sub-types include:
*
* - NOP (for record_login/logout)
* - new shared secret from last re-key
*/
static int aps_started = 0;
static int is_monitor = 0;
static pid_t monitor_pid, child_pid;
static int pipe_fds[2];
static int pipe_fd = -1;
static Buffer input_pipe, output_pipe; /* for pipe I/O */
static Authctxt *xxx_authctxt;
/* Monitor functions */
extern void aps_monitor_loop(Authctxt *authctxt, int pipe, pid_t child_pid);
static void aps_record_login(void);
static void aps_record_logout(void);
/* Altprivsep packet utilities for communication with the monitor */
static void altprivsep_packet_start(u_char);
static int altprivsep_packet_send(void);
static int altprivsep_fwd_packet(u_char type);
static int altprivsep_packet_read(void);
static void altprivsep_packet_read_expect(int type);
static void altprivsep_packet_put_char(int ch);
static void altprivsep_packet_put_int(u_int value);
static void altprivsep_packet_put_cstring(const char *str);
static void altprivsep_packet_put_raw(const void *buf, u_int len);
static u_int altprivsep_packet_get_char(void);
static void *altprivsep_packet_get_raw(u_int *length_ptr);
static void *altprivsep_packet_get_string(u_int *length_ptr);
/*
* Start monitor from privileged sshd process.
*
* Return values are like fork(2); the parent is the monitor. The caller should
* fatal() on error.
*
* Privileges are dropped, on the unprivileged side, upon success.
*/
pid_t
altprivsep_start_monitor(Authctxt *authctxt)
{
pid_t pid;
int junk;
if (aps_started || authctxt == NULL || authctxt->pw == NULL)
fatal("Monitor startup failed: missing state");
xxx_authctxt = authctxt;
packet_set_server();
buffer_init(&output_pipe);
buffer_init(&input_pipe);
if (pipe(pipe_fds) != 0) {
error("Monitor startup failure: could not create pipes: %s",
strerror(errno));
return (-1);
}
(void) fcntl(pipe_fds[0], F_SETFD, FD_CLOEXEC);
(void) fcntl(pipe_fds[1], F_SETFD, FD_CLOEXEC);
monitor_pid = getpid();
if ((pid = fork()) > 0) {
/* parent */
child_pid = pid;
debug2("Monitor pid %ld, unprivileged child pid %ld",
monitor_pid, child_pid);
(void) close(pipe_fds[1]);
pipe_fd = pipe_fds[0];
if (fcntl(pipe_fd, F_SETFL, O_NONBLOCK) < 0)
error("fcntl O_NONBLOCK: %.100s", strerror(errno));
/* signal readyness of monitor */
(void) write(pipe_fd, &pid, sizeof (pid));
aps_started = 1;
is_monitor = 1;
debug2("Monitor started");
set_log_txt_prefix("monitor ");
return (pid);
}
if (pid < 0) {
debug2("Monitor startup failure: could not fork unprivileged"
" process: %s", strerror(errno));
return (pid);
}
/* caller should drop privs */
(void) close(pipe_fds[0]);
pipe_fd = pipe_fds[1];
/* wait for monitor to be ready */
debug2("Waiting for monitor");
(void) read(pipe_fd, &junk, sizeof (junk));
debug2("Monitor signalled readiness");
if (fcntl(pipe_fd, F_SETFL, O_NONBLOCK) < 0)
error("fcntl O_NONBLOCK: %.100s", strerror(errno));
buffer_init(&to_monitor);
buffer_init(&from_monitor);
if (compat20) {
debug3("Setting handler to forward re-key packets to monitor");
dispatch_range(SSH2_MSG_KEXINIT, SSH2_MSG_TRANSPORT_MAX,
&altprivsep_rekey);
}
/* AltPrivSep interfaces are set up */
aps_started = 1;
return (pid);
}
int
altprivsep_get_pipe_fd(void)
{
return (pipe_fd);
}
void
altprivsep_rekey(int type, u_int32_t seq, void *ctxt)
{
Kex *kex = (Kex *)ctxt;
if (kex == NULL)
fatal("Missing key exchange context in unprivileged process");
debug2("Forwarding re-key packet (%d) to monitor", type);
if (type != SSH2_MSG_NEWKEYS)
if (!altprivsep_fwd_packet(type))
fatal("Monitor not responding");
/* tell server_loop2() that we're re-keying */
kex->done = 0;
/* NEWKEYS is special: get the new keys for client->server direction */
if (type == SSH2_MSG_NEWKEYS) {
debug2("Getting new inbound keystate from monitor");
altprivsep_get_newkeys(MODE_IN);
kex->done = 1;
}
}
void
altprivsep_process_input(Kex *kex, fd_set *rset)
{
void *data;
int type;
u_int dlen;
debug2("Reading from pipe to monitor (%d)", pipe_fd);
if (pipe_fd == -1)
return;
if (!FD_ISSET(pipe_fd, rset))
return;
if ((type = altprivsep_packet_read()) == -1)
fatal("Monitor not responding");
if (!compat20)
return; /* shouldn't happen! but be safe */
if (type == 0)
return; /* EOF -- nothing to do here */
if (type >= SSH2_MSG_MAX)
fatal("Received garbage from monitor");
debug2("Read packet type %d from pipe to monitor", (u_int)type);
if (type == SSH2_PRIV_MSG_ALTPRIVSEP)
return; /* shouldn't happen! */
/* NEWKEYS is special: get the new keys for server->client direction */
if (type == SSH2_MSG_NEWKEYS) {
debug2("Getting new outbound keystate from monitor");
packet_start(SSH2_MSG_NEWKEYS);
packet_send();
altprivsep_get_newkeys(MODE_OUT);
return;
}
data = altprivsep_packet_get_raw(&dlen);
packet_start((u_char)type);
if (data != NULL && dlen > 0)
packet_put_raw(data, dlen);
packet_send();
}
void
altprivsep_do_monitor(Authctxt *authctxt, pid_t child_pid)
{
aps_monitor_loop(authctxt, pipe_fd, child_pid);
}
int
altprivsep_started(void)
{
return (aps_started);
}
int
altprivsep_is_monitor(void)
{
return (is_monitor);
}
/*
* A fatal cleanup function to forcibly shutdown the connection socket
*/
void
altprivsep_shutdown_sock(void *arg)
{
int sock;
if (arg == NULL)
return;
sock = *(int *)arg;
(void) shutdown(sock, SHUT_RDWR);
}
/* Calls _to_ monitor from unprivileged process */
static
int
altprivsep_fwd_packet(u_char type)
{
u_int len;
void *data;
altprivsep_packet_start(type);
data = packet_get_raw(&len);
altprivsep_packet_put_raw(data, len);
/* packet_send()s any replies from the monitor to the client */
return (altprivsep_packet_send());
}
extern Newkeys *current_keys[MODE_MAX];
/* To be called from packet.c:set_newkeys() before referencing current_keys */
void
altprivsep_get_newkeys(enum kex_modes mode)
{
Newkeys *newkeys;
Comp *comp;
Enc *enc;
Mac *mac;
u_int len;
if (!altprivsep_started())
return;
if (altprivsep_is_monitor())
return; /* shouldn't happen */
/* request new keys */
altprivsep_packet_start(SSH2_PRIV_MSG_ALTPRIVSEP);
altprivsep_packet_put_char(APS_MSG_NEWKEYS_REQ);
altprivsep_packet_put_int((u_int)mode);
altprivsep_packet_send();
altprivsep_packet_read_expect(SSH2_PRIV_MSG_ALTPRIVSEP);
if (altprivsep_packet_get_char() != APS_MSG_NEWKEYS_REP)
fatal("Received garbage from monitor during re-keying");
newkeys = xmalloc(sizeof (*newkeys));
memset(newkeys, 0, sizeof (*newkeys));
enc = &newkeys->enc;
mac = &newkeys->mac;
comp = &newkeys->comp;
/* Cipher name, key, IV */
enc->name = altprivsep_packet_get_string(NULL);
if ((enc->cipher = cipher_by_name(enc->name)) == NULL)
fatal("Monitor negotiated an unknown cipher during re-key");
enc->key = altprivsep_packet_get_string(&enc->key_len);
enc->iv = altprivsep_packet_get_string(&enc->block_size);
/* MAC name */
mac->name = altprivsep_packet_get_string(NULL);
if (mac_init(mac, mac->name) < 0)
fatal("Monitor negotiated an unknown MAC algorithm "
"during re-key");
mac->key = altprivsep_packet_get_string(&len);
if (len > mac->key_len)
fatal("%s: bad mac key length: %d > %d", __func__, len,
mac->key_len);
/* Compression algorithm name */
comp->name = altprivsep_packet_get_string(NULL);
if (strcmp(comp->name, "zlib") != 0 && strcmp(comp->name, "none") != 0)
fatal("Monitor negotiated an unknown compression "
"algorithm during re-key");
comp->type = 0;
comp->enabled = 0; /* forces compression re-init, as per-spec */
if (strcmp(comp->name, "zlib") == 0)
comp->type = 1;
/*
* Now install new keys
*
* For now abuse kex.c/packet.c non-interfaces. Someday, when
* the many internal interfaces are parametrized, made reentrant
* and thread-safe, made more consistent, and when necessary-but-
* currently-missing interfaces are added then this bit of
* ugliness can be revisited.
*
* The ugliness is in the set_newkeys(), its name and the lack
* of a (Newkeys *) parameter, which forces us to pass the
* newkeys through current_keys[mode]. But this saves us some
* lines of code for now, though not comments.
*
* Also, we've abused, in the code above, knowledge of what
* set_newkeys() expects the current_keys[mode] to contain.
*/
current_keys[mode] = newkeys;
set_newkeys(mode);
}
void
altprivsep_record_login(pid_t pid, const char *ttyname)
{
altprivsep_packet_start(SSH2_PRIV_MSG_ALTPRIVSEP);
altprivsep_packet_put_char(APS_MSG_RECORD_LOGIN);
altprivsep_packet_put_int(pid);
altprivsep_packet_put_cstring(ttyname);
altprivsep_packet_send();
altprivsep_packet_read_expect(SSH2_PRIV_MSG_ALTPRIVSEP);
}
void
altprivsep_record_logout(pid_t pid)
{
altprivsep_packet_start(SSH2_PRIV_MSG_ALTPRIVSEP);
altprivsep_packet_put_char(APS_MSG_RECORD_LOGOUT);
altprivsep_packet_put_int(pid);
altprivsep_packet_send();
altprivsep_packet_read_expect(SSH2_PRIV_MSG_ALTPRIVSEP);
}
static void aps_send_newkeys(void);
/* Monitor side dispatch handler for SSH2_PRIV_MSG_ALTPRIVSEP */
/* ARGSUSED */
void
aps_input_altpriv_msg(int type, u_int32_t seq, void *ctxt)
{
u_char req_type;
req_type = packet_get_char();
switch (req_type) {
case APS_MSG_NEWKEYS_REQ:
aps_send_newkeys();
break;
case APS_MSG_RECORD_LOGIN:
aps_record_login();
break;
case APS_MSG_RECORD_LOGOUT:
aps_record_logout();
break;
default:
break;
}
}
/* Monitor-side handlers for APS_MSG_* */
static
void
aps_send_newkeys(void)
{
Newkeys *newkeys;
Enc *enc;
Mac *mac;
Comp *comp;
enum kex_modes mode;
/* get direction for which newkeys are wanted */
mode = (enum kex_modes) packet_get_int();
packet_check_eom();
/* get those newkeys */
newkeys = kex_get_newkeys(mode);
enc = &newkeys->enc;
mac = &newkeys->mac;
comp = &newkeys->comp;
/*
* Negotiated algorithms, client->server and server->client, for
* cipher, mac and compression.
*/
packet_start(SSH2_PRIV_MSG_ALTPRIVSEP);
packet_put_char(APS_MSG_NEWKEYS_REP);
packet_put_cstring(enc->name);
packet_put_string(enc->key, enc->key_len);
packet_put_string(enc->iv, enc->block_size);
packet_put_cstring(mac->name);
packet_put_string(mac->key, mac->key_len);
packet_put_cstring(comp->name);
packet_send();
}
struct _aps_login_rec {
pid_t lr_pid;
char *lr_tty;
struct _aps_login_rec *next;
};
typedef struct _aps_login_rec aps_login_rec;
static aps_login_rec *aps_login_list = NULL;
static
void
aps_record_login(void)
{
aps_login_rec *new_rec;
struct stat sbuf;
size_t proc_path_len;
char *proc_path;
new_rec = xmalloc(sizeof (aps_login_rec));
memset(new_rec, 0, sizeof (aps_login_rec));
new_rec->lr_pid = packet_get_int();
new_rec->lr_tty = packet_get_string(NULL);
proc_path_len = snprintf(NULL, 0, "/proc/%d", new_rec->lr_pid);
proc_path = xmalloc(proc_path_len + 1);
(void) snprintf(proc_path, proc_path_len + 1, "/proc/%d",
new_rec->lr_pid);
if (stat(proc_path, &sbuf) ||
sbuf.st_uid != xxx_authctxt->pw->pw_uid ||
stat(new_rec->lr_tty, &sbuf) < 0 ||
sbuf.st_uid != xxx_authctxt->pw->pw_uid) {
debug2("Spurious record_login request from unprivileged sshd");
xfree(proc_path);
xfree(new_rec->lr_tty);
xfree(new_rec);
return;
}
/* Insert new record on list */
new_rec->next = aps_login_list;
aps_login_list = new_rec;
record_login(new_rec->lr_pid, new_rec->lr_tty, NULL,
xxx_authctxt->user);
packet_start(SSH2_PRIV_MSG_ALTPRIVSEP);
packet_send();
xfree(proc_path);
}
static
void
aps_record_logout(void)
{
aps_login_rec **p, *q;
pid_t pid;
pid = packet_get_int();
packet_check_eom();
for (p = &aps_login_list; *p != NULL; p = &q->next) {
q = *p;
if (q->lr_pid == pid) {
record_logout(q->lr_pid, q->lr_tty, NULL,
xxx_authctxt->user);
/* dequeue */
*p = q->next;
xfree(q->lr_tty);
xfree(q);
break;
}
}
packet_start(SSH2_PRIV_MSG_ALTPRIVSEP);
packet_send();
}
/* Utilities for communication with the monitor */
static
void
altprivsep_packet_start(u_char type)
{
buffer_clear(&to_monitor);
buffer_put_char(&to_monitor, type);
}
static
void
altprivsep_packet_put_char(int ch)
{
buffer_put_char(&to_monitor, ch);
}
static
void
altprivsep_packet_put_int(u_int value)
{
buffer_put_int(&to_monitor, value);
}
static
void
altprivsep_packet_put_cstring(const char *str)
{
buffer_put_cstring(&to_monitor, str);
}
static
void
altprivsep_packet_put_raw(const void *buf, u_int len)
{
buffer_append(&to_monitor, buf, len);
}
/*
* Send a monitor packet to the monitor. This function is blocking.
*
* Returns -1 if the monitor pipe has been closed earlier, fatal()s if
* there's any other problems.
*/
static
int
altprivsep_packet_send(void)
{
ssize_t len;
u_int32_t plen; /* packet length */
u_char plen_buf[sizeof (plen)];
u_char padlen; /* padding length */
fd_set *setp;
if (pipe_fd == -1)
return (-1);
if ((plen = buffer_len(&to_monitor)) == 0)
return (0);
/*
* We talk the SSHv2 binary packet protocol to the monitor,
* using the none cipher, mac and compression algorithms.
*
* But, interestingly, the none cipher has a block size of 8
* bytes, thus we must pad the packet.
*
* Also, encryption includes the packet length, so the padding
* must account for that field. I.e., (sizeof (packet length) +
* sizeof (padding length) + packet length + padding length) %
* block_size must == 0.
*
* Also, there must be at least four (4) bytes of padding.
*/
padlen = (8 - ((plen + sizeof (plen) + sizeof (padlen)) % 8)) % 8;
if (padlen < 4)
padlen += 8;
/* packet length counts padding and padding length field */
plen += padlen + sizeof (padlen);
PUT_32BIT(plen_buf, plen);
setp = xmalloc(howmany(pipe_fd + 1, NFDBITS) * sizeof (fd_mask));
memset(setp, 0, howmany(pipe_fd + 1, NFDBITS) * sizeof (fd_mask));
FD_SET(pipe_fd, setp);
while (select(pipe_fd + 1, NULL, setp, NULL, NULL) == -1) {
if (errno == EAGAIN || errno == EINTR)
continue;
else
goto pipe_gone;
}
xfree(setp);
/* packet length field */
len = atomicio(write, pipe_fd, plen_buf, sizeof (plen));
if (len != sizeof (plen))
goto pipe_gone;
/* padding length field */
len = atomicio(write, pipe_fd, &padlen, sizeof (padlen));
if (len != sizeof (padlen))
goto pipe_gone;
len = atomicio(write, pipe_fd, buffer_ptr(&to_monitor), plen - 1);
if (len != (plen - 1))
goto pipe_gone;
buffer_clear(&to_monitor);
return (1);
pipe_gone:
(void) close(pipe_fd);
pipe_fd = -1;
fatal("Monitor not responding");
/* NOTREACHED */
return (0);
}
/*
* Read a monitor packet from the monitor. This function is blocking.
*/
static
int
altprivsep_packet_read(void)
{
ssize_t len = -1;
u_int32_t plen;
u_char plen_buf[sizeof (plen)];
u_char padlen;
fd_set *setp;
if (pipe_fd == -1)
return (-1);
setp = xmalloc(howmany(pipe_fd + 1, NFDBITS) * sizeof (fd_mask));
memset(setp, 0, howmany(pipe_fd + 1, NFDBITS) * sizeof (fd_mask));
FD_SET(pipe_fd, setp);
while (select(pipe_fd + 1, setp, NULL, NULL, NULL) == -1) {
if (errno == EAGAIN || errno == EINTR)
continue;
else
goto pipe_gone;
}
xfree(setp);
/* packet length field */
len = atomicio(read, pipe_fd, plen_buf, sizeof (plen));
plen = GET_32BIT(plen_buf);
if (len != sizeof (plen))
goto pipe_gone;
/* padding length field */
len = atomicio(read, pipe_fd, &padlen, sizeof (padlen));
if (len != sizeof (padlen))
goto pipe_gone;
plen -= sizeof (padlen);
buffer_clear(&from_monitor);
buffer_append_space(&from_monitor, plen);
/* packet data + padding */
len = atomicio(read, pipe_fd, buffer_ptr(&from_monitor), plen);
if (len != plen)
goto pipe_gone;
/* remove padding */
if (padlen > 0)
buffer_consume_end(&from_monitor, padlen);
/* packet type */
return (buffer_get_char(&from_monitor));
pipe_gone:
(void) close(pipe_fd);
pipe_fd = -1;
if (len < 0)
fatal("Monitor not responding");
debug2("Monitor pipe closed by monitor");
return (0);
}
static
void
altprivsep_packet_read_expect(int expected)
{
int type;
type = altprivsep_packet_read();
if (type <= 0)
fatal("Monitor not responding");
if (type != expected)
fatal("Protocol error in privilege separation; expected "
"packet type %d, got %d", expected, type);
}
static
u_int
altprivsep_packet_get_char(void)
{
return (buffer_get_char(&from_monitor));
}
void
*altprivsep_packet_get_raw(u_int *length_ptr)
{
if (length_ptr != NULL)
*length_ptr = buffer_len(&from_monitor);
return (buffer_ptr(&from_monitor));
}
void
*altprivsep_packet_get_string(u_int *length_ptr)
{
return (buffer_get_string(&from_monitor, length_ptr));
}