/*
* Amanda, The Advanced Maryland Automatic Network Disk Archiver
* Copyright (c) 1999 University of Maryland
* All Rights Reserved.
*
* Permission to use, copy, modify, distribute, and sell this software and its
* documentation for any purpose is hereby granted without fee, provided that
* the above copyright notice appear in all copies and that both that
* copyright notice and this permission notice appear in supporting
* documentation, and that the name of U.M. not be used in advertising or
* publicity pertaining to distribution of the software without specific,
* written prior permission. U.M. makes no representations about the
* suitability of this software for any purpose. It is provided "as is"
* without express or implied warranty.
*
* U.M. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL U.M.
* BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
* Authors: the Amanda Development Team. Its members are listed in a
* file named AUTHORS, in the root directory of this distribution.
*/
/*
* $Id: krb5-security.c,v 1.22 2006/06/16 10:55:05 martinea Exp $
*
* krb5-security.c - kerberos V5 security module
*
* XXX still need to check for initial keyword on connect so we can skip
* over shell garbage and other stuff that krb5 might want to spew out.
*/
#include "amanda.h"
#include "amutil.h"
#include "event.h"
#include "packet.h"
#include "security.h"
#include "security-util.h"
#include "stream.h"
#include "sockaddr-util.h"
#ifdef KRB5_HEIMDAL_INCLUDES
#include "com_err.h"
#endif
#define BROKEN_MEMORY_CCACHE
#ifdef BROKEN_MEMORY_CCACHE
/*
* If you don't have atexit() or on_exit(), you could just consider
* making atexit() empty and clean up your ticket files some other
* way
*/
#ifndef HAVE_ATEXIT
#ifdef HAVE_ON_EXIT
#define atexit(func) on_exit(func, 0)
#else
#error "You don't have atexit() or on_exit()"
#endif /* HAVE_ON_EXIT */
#endif /* ! HAVE_ATEXIT */
#endif
#ifndef KRB5_HEIMDAL_INCLUDES
#include <gssapi/gssapi_generic.h>
#else
#include <gssapi/gssapi.h>
#endif
#include <krb5.h>
#ifndef KRB5_ENV_CCNAME
#define KRB5_ENV_CCNAME "KRB5CCNAME"
#endif
/*
* consider undefining when kdestroy() is fixed. The current version does
* not work under krb5-1.2.4 in rh7.3, perhaps others.
*/
#define KDESTROY_VIA_UNLINK 1
/*
* Where the keytab lives, if defined. Otherwise it expects something in the
* config file.
*/
/* #define AMANDA_KEYTAB "/.amanda-v5-keytab" */
/*
* The name of the principal we authenticate with, if defined. Otherwise
* it expects something in the config file.
*/
/* #define AMANDA_PRINCIPAL "service/amanda" */
/*
* The lifetime of our tickets in seconds. This may or may not need to be
* configurable.
*/
#define AMANDA_TKT_LIFETIME (12*60*60)
/*
* The name of the service in /etc/services. This probably shouldn't be
* configurable.
*/
#define AMANDA_KRB5_SERVICE_NAME "k5amanda"
/*
* The default port to use if above entry in /etc/services doesn't exist
*/
#define AMANDA_KRB5_DEFAULT_PORT 10082
/*
* The timeout in seconds for each step of the GSS negotiation phase
*/
#define GSS_TIMEOUT 30
/*
* This is the tcp stream buffer size
*/
#define KRB5_STREAM_BUFSIZE (32768 * 2)
/*
* This is the max number of outgoing connections we can have at once.
* planner/amcheck/etc will open a bunch of connections as it tries
* to contact everything. We need to limit this to avoid blowing
* the max number of open file descriptors a process can have.
*/
#define AMANDA_KRB5_MAXCONN 40
/*
* Number of seconds krb5 has to start up
*/
#define CONNECT_TIMEOUT 20
/*
* Cache the local hostname
*/
static char myhostname[MAX_HOSTNAME_LENGTH+1];
/*
* Interface functions
*/
static void krb5_accept(const struct security_driver *,
char *(*)(char *, void *),
int, int,
void (*)(security_handle_t *, pkt_t *),
void *);
static void krb5_connect(const char *,
char *(*)(char *, void *),
void (*)(void *, security_handle_t *, security_status_t), void *, void *);
static void krb5_init(void);
#ifdef BROKEN_MEMORY_CCACHE
static void cleanup(void);
#endif
static const char *get_tgt(char *keytab_name, char *principal_name);
static int gss_server(struct tcp_conn *);
static int gss_client(struct sec_handle *);
static const char *gss_error(OM_uint32, OM_uint32);
static char *krb5_checkuser(char *host, char *name, char *realm);
static int k5_encrypt(void *cookie, void *buf, ssize_t buflen,
void **encbuf, ssize_t *encbuflen);
static int k5_decrypt(void *cookie, void *buf, ssize_t buflen,
void **encbuf, ssize_t *encbuflen);
static ssize_t krb5_tcpm_recv_token(struct tcp_conn *rc, int *handle,
char **errmsg, char **buf, ssize_t *size,
int timeout);
/*
* This is our interface to the outside world.
*/
const security_driver_t krb5_security_driver = {
"KRB5",
krb5_connect,
krb5_accept,
sec_get_authenticated_peer_name_hostname,
sec_close,
stream_sendpkt,
stream_recvpkt,
stream_recvpkt_cancel,
tcpma_stream_server,
tcpma_stream_accept,
tcpma_stream_client,
tcpma_stream_close,
tcpma_stream_close_async,
sec_stream_auth,
sec_stream_id,
tcpm_stream_write,
tcpm_stream_write_async,
tcpm_stream_read,
tcpm_stream_read_sync,
tcpm_stream_read_to_shm_ring,
tcpm_stream_read_cancel,
tcpm_stream_pause,
tcpm_stream_resume,
tcpm_close_connection,
k5_encrypt,
k5_decrypt,
generic_data_write,
generic_data_write_non_blocking,
generic_data_read
};
static int newhandle = 1;
/*
* Local functions
*/
static int runkrb5(struct sec_handle *);
char *keytab_name;
char *principal_name;
/*
* krb5 version of a security handle allocator. Logically sets
* up a network "connection".
*/
static void
krb5_connect(
const char *hostname,
char * (*conf_fn)(char *, void *),
void (*fn)(void *, security_handle_t *, security_status_t),
void * arg,
void * datap)
{
struct sec_handle *rh;
int result;
char *canonname;
assert(fn != NULL);
assert(hostname != NULL);
auth_debug(1, "krb5: krb5_connect: %s\n", hostname);
krb5_init();
rh = g_malloc(sizeof(*rh));
security_handleinit(&rh->sech, &krb5_security_driver);
rh->dle_hostname = g_strdup(hostname);
rh->hostname = NULL;
rh->rs = NULL;
rh->ev_timeout = NULL;
rh->rc = NULL;
result = resolve_hostname(hostname, 0, NULL, &canonname);
if(result != 0) {
dbprintf(_("resolve_hostname(%s): %s\n"), hostname, gai_strerror(result));
security_seterror(&rh->sech, _("resolve_hostname(%s): %s"), hostname,
gai_strerror(result));
(*fn)(arg, &rh->sech, S_ERROR);
return;
}
if (canonname == NULL) {
dbprintf(_("resolve_hostname(%s) did not return a canonical name\n"), hostname);
security_seterror(&rh->sech,
_("resolve_hostname(%s) did not return a canonical name"), hostname);
(*fn)(arg, &rh->sech, S_ERROR);
return;
}
rh->hostname = canonname; /* will be replaced */
canonname = NULL; /* steal reference */
rh->rs = tcpma_stream_client(rh, newhandle++);
if (rh->rc == NULL)
goto error;
rh->rc->conf_fn = conf_fn;
rh->rc->datap = datap;
rh->rc->recv_security_ok = NULL;
rh->rc->prefix_packet = NULL;
rh->rc->need_priv_port = 0;
if (rh->rs == NULL)
goto error;
amfree(rh->hostname);
rh->hostname = g_strdup(rh->rs->rc->hostname);
#ifdef AMANDA_KEYTAB
keytab_name = AMANDA_KEYTAB;
#else
if(conf_fn) {
keytab_name = conf_fn("krb5keytab", datap);
}
#endif
#ifdef AMANDA_PRINCIPAL
principal_name = AMANDA_PRINCIPAL;
#else
if(conf_fn) {
principal_name = conf_fn("krb5principal", datap);
}
#endif
/*
* We need to open a new connection.
*
* XXX need to eventually limit number of outgoing connections here.
*/
if(rh->rc->read == -1) {
if (runkrb5(rh) < 0)
goto error;
rh->rc->refcnt++;
}
/*
* The socket will be opened async so hosts that are down won't
* block everything. We need to register a write event
* so we will know when the socket comes alive.
*
* Overload rh->rs->ev_read to provide a write event handle.
* We also register a timeout.
*/
g_mutex_lock(security_mutex);
rh->fn.connect = fn;
rh->arg = arg;
rh->rs->rc->ev_write = event_create((event_id_t)(rh->rs->rc->write),
EV_WRITEFD, sec_connect_callback, rh);
rh->ev_timeout = event_create(CONNECT_TIMEOUT, EV_TIME,
sec_connect_timeout, rh);
event_activate(rh->rs->rc->ev_write);
event_activate(rh->ev_timeout);
g_mutex_unlock(security_mutex);
amfree(canonname);
return;
error:
amfree(canonname);
(*fn)(arg, &rh->sech, S_ERROR);
}
/*
* Setup to handle new incoming connections
*/
static void
krb5_accept(
const struct security_driver *driver,
char *(*conf_fn)(char *, void *),
int in,
int out,
void (*fn)(security_handle_t *, pkt_t *),
void *datap)
{
sockaddr_union sin;
socklen_t_equiv len;
struct tcp_conn *rc;
char hostname[NI_MAXHOST];
int result;
char *errmsg = NULL;
struct passwd *pw;
krb5_init();
len = sizeof(sin);
if (getpeername(in, (struct sockaddr *)&sin, &len) < 0) {
dbprintf(_("getpeername returned: %s\n"),
strerror(errno));
return;
}
if ((result = getnameinfo((struct sockaddr *)&sin, len,
hostname, NI_MAXHOST, NULL, 0, 0) != 0)) {
dbprintf(_("getnameinfo failed: %s\n"),
gai_strerror(result));
return;
}
if (check_name_give_sockaddr(hostname,
(struct sockaddr *)&sin, &errmsg) < 0) {
dbprintf(_("check_name_give_sockaddr(%s): %s\n"),
hostname, errmsg);
amfree(errmsg);
return;
}
rc = sec_tcp_conn_get(NULL, hostname, 0);
rc->conf_fn = conf_fn;
rc->datap = datap;
rc->recv_security_ok = NULL;
rc->prefix_packet = NULL;
rc->need_priv_port = 0;
copy_sockaddr(&rc->peer, &sin);
rc->read = in;
rc->write = out;
rc->driver = driver;
if (gss_server(rc) < 0)
error("gss_server failed: %s\n", rc->errmsg);
rc->accept_fn = fn;
sec_tcp_conn_read(rc);
/* totally drop privileges at this point
*(making the userid equal to the dumpuser)
*/
pw = getpwnam(CLIENT_LOGIN);
if (setreuid(pw->pw_uid, pw->pw_uid) == -1) {
g_critical("setreuid failed: %s", strerror(errno));
exit(1);
}
}
/*
* Forks a krb5 to the host listed in rc->hostname
* Returns negative on error, with an errmsg in rc->errmsg.
*/
static int
runkrb5(
struct sec_handle * rh)
{
int server_socket;
in_port_t my_port, port;
struct tcp_conn * rc = rh->rc;
const char *err;
char *stream_msg = NULL;
port = find_port_for_service(AMANDA_KRB5_SERVICE_NAME, "tcp");
if ((err = get_tgt(keytab_name, principal_name)) != NULL) {
security_seterror(&rh->sech, "%s: could not get TGT: %s",
rc->hostname, err);
return -1;
}
server_socket = stream_client_privileged(NULL, rc->hostname,
port,
STREAM_BUFSIZE,
STREAM_BUFSIZE,
&my_port,
0, &stream_msg);
if (stream_msg) {
security_seterror(&rh->sech, "%s", stream_msg);
g_free(stream_msg);
return -1;
}
if (server_socket < 0) {
security_seterror(&rh->sech, "%s", strerror(errno));
return -1;
}
rc->read = rc->write = server_socket;
if (gss_client(rh) < 0) {
return -1;
}
return 0;
}
/*
* Negotiate a krb5 gss context from the client end.
*/
static int
gss_client(
struct sec_handle *rh)
{
struct sec_stream *rs = rh->rs;
struct tcp_conn *rc = rs->rc;
gss_buffer_desc send_tok, recv_tok, AA;
gss_OID doid;
OM_uint32 maj_stat, min_stat;
unsigned int ret_flags;
int rval = -1;
int rvalue;
gss_name_t gss_name;
char *errmsg = NULL;
auth_debug(1, "gss_client\n");
send_tok.value = g_strjoin(NULL, "host/", rs->rc->hostname, NULL);
send_tok.length = strlen(send_tok.value) + 1;
maj_stat = gss_import_name(&min_stat, &send_tok, GSS_C_NULL_OID,
&gss_name);
if (maj_stat != (OM_uint32)GSS_S_COMPLETE) {
security_seterror(&rh->sech, _("can't import name %s: %s"),
(char *)send_tok.value, gss_error(maj_stat, min_stat));
amfree(send_tok.value);
return (-1);
}
amfree(send_tok.value);
rc->gss_context = GSS_C_NO_CONTEXT;
maj_stat = gss_display_name(&min_stat, gss_name, &AA, &doid);
dbprintf(_("gss_name %s\n"), (char *)AA.value);
/*
* Perform the context-establishement loop.
*
* Every generated token is stored in send_tok which is then
* transmitted to the server; every received token is stored in
* recv_tok (empty on the first pass) to be processed by
* the next call to gss_init_sec_context.
*
* GSS-API guarantees that send_tok's length will be non-zero
* if and only if the server is expecting another token from us,
* and that gss_init_sec_context returns GSS_S_CONTINUE_NEEDED if
* and only if the server has another token to send us.
*/
recv_tok.value = NULL;
for (recv_tok.length = 0;;) {
min_stat = 0;
maj_stat = gss_init_sec_context(&min_stat,
GSS_C_NO_CREDENTIAL,
&rc->gss_context,
gss_name,
GSS_C_NULL_OID,
(OM_uint32)GSS_C_MUTUAL_FLAG|GSS_C_REPLAY_FLAG,
0, NULL, /* no channel bindings */
(recv_tok.length == 0 ? GSS_C_NO_BUFFER : &recv_tok),
NULL, /* ignore mech type */
&send_tok,
&ret_flags,
NULL); /* ignore time_rec */
if (recv_tok.length != 0) {
amfree(recv_tok.value);
recv_tok.length = 0;
}
if (maj_stat != (OM_uint32)GSS_S_COMPLETE && maj_stat != (OM_uint32)GSS_S_CONTINUE_NEEDED) {
security_seterror(&rh->sech,
_("error getting gss context: %s %s"),
gss_error(maj_stat, min_stat), (char *)send_tok.value);
goto done;
}
/*
* Send back the response
*/
if (send_tok.length != 0 && tcpm_send_token(rc, rs->handle, &errmsg, send_tok.value, send_tok.length) < 0) {
security_seterror(&rh->sech, "%s", rc->errmsg);
gss_release_buffer(&min_stat, &send_tok);
goto done;
}
gss_release_buffer(&min_stat, &send_tok);
/*
* If we need to continue, then register for more packets
*/
if (maj_stat != (OM_uint32)GSS_S_CONTINUE_NEEDED)
break;
rvalue = krb5_tcpm_recv_token(rc, &rc->handle,
&rc->errmsg,
(void *)&recv_tok.value,
(ssize_t *)&recv_tok.length, 60);
if (rvalue <= 0) {
if (rvalue < 0)
security_seterror(&rh->sech,
_("recv error in gss loop: %s"), rc->errmsg);
else
security_seterror(&rh->sech, _("EOF in gss loop"));
goto done;
}
}
rval = 0;
rc->auth = 1;
done:
gss_release_name(&min_stat, &gss_name);
return (rval);
}
/*
* Negotiate a krb5 gss context from the server end.
*/
static int
gss_server(
struct tcp_conn *rc)
{
OM_uint32 maj_stat, min_stat, ret_flags;
gss_buffer_desc send_tok, recv_tok, AA;
gss_OID doid;
gss_name_t gss_name;
gss_cred_id_t gss_creds;
char *p, *realm, *msg;
int rval = -1;
int rvalue;
char errbuf[256];
char *errmsg = NULL;
auth_debug(1, "gss_server\n");
assert(rc != NULL);
/*
* We need to be root while in gss_acquire_cred() to read the host key
* out of the default keytab. We also need to be root in
* gss_accept_context() thanks to the replay cache code.
*/
if (!set_root_privs(0)) {
g_snprintf(errbuf, sizeof(errbuf),
_("can't take root privileges to read krb5 host key: %s"), strerror(errno));
goto out;
}
rc->gss_context = GSS_C_NO_CONTEXT;
send_tok.value = g_strjoin(NULL, "host/", myhostname, NULL);
send_tok.length = strlen(send_tok.value) + 1;
for (p = send_tok.value; *p != '\0'; p++) {
if (isupper((int)*p))
*p = tolower(*p);
}
maj_stat = gss_import_name(&min_stat, &send_tok, GSS_C_NULL_OID,
&gss_name);
if (maj_stat != (OM_uint32)GSS_S_COMPLETE) {
set_root_privs(0);
g_snprintf(errbuf, sizeof(errbuf),
_("can't import name %s: %s"), (char *)send_tok.value,
gss_error(maj_stat, min_stat));
amfree(send_tok.value);
goto out;
}
amfree(send_tok.value);
maj_stat = gss_display_name(&min_stat, gss_name, &AA, &doid);
dbprintf(_("gss_name %s\n"), (char *)AA.value);
maj_stat = gss_acquire_cred(&min_stat, gss_name, 0,
GSS_C_NULL_OID_SET, GSS_C_ACCEPT, &gss_creds, NULL, NULL);
if (maj_stat != (OM_uint32)GSS_S_COMPLETE) {
g_snprintf(errbuf, sizeof(errbuf),
_("can't acquire creds for host key host/%s: %s"), myhostname,
gss_error(maj_stat, min_stat));
gss_release_name(&min_stat, &gss_name);
set_root_privs(0);
goto out;
}
gss_release_name(&min_stat, &gss_name);
for (recv_tok.length = 0;;) {
recv_tok.value = NULL;
rvalue = krb5_tcpm_recv_token(rc, &rc->handle,
&rc->errmsg,
/* (void *) is to avoid type-punning warning */
(char **)(void *)&recv_tok.value,
(ssize_t *)&recv_tok.length, 60);
if (rvalue <= 0) {
if (rvalue < 0) {
g_snprintf(errbuf, sizeof(errbuf),
_("recv error in gss loop: %s"), rc->errmsg);
amfree(rc->errmsg);
} else
g_snprintf(errbuf, sizeof(errbuf), _("EOF in gss loop"));
goto out;
}
maj_stat = gss_accept_sec_context(&min_stat, &rc->gss_context,
gss_creds, &recv_tok, GSS_C_NO_CHANNEL_BINDINGS,
&gss_name, &doid, &send_tok, &ret_flags, NULL, NULL);
if (maj_stat != (OM_uint32)GSS_S_COMPLETE &&
maj_stat != (OM_uint32)GSS_S_CONTINUE_NEEDED) {
g_snprintf(errbuf, sizeof(errbuf),
_("error accepting context: %s"), gss_error(maj_stat, min_stat));
amfree(recv_tok.value);
goto out;
}
amfree(recv_tok.value);
if (send_tok.length != 0 && tcpm_send_token(rc, 0, &errmsg, send_tok.value, send_tok.length) < 0) {
strncpy(errbuf, rc->errmsg, sizeof(errbuf) - 1);
errbuf[sizeof(errbuf) - 1] = '\0';
amfree(rc->errmsg);
gss_release_buffer(&min_stat, &send_tok);
goto out;
}
gss_release_buffer(&min_stat, &send_tok);
/*
* If we need to get more from the client, then register for
* more packets.
*/
if (maj_stat != (OM_uint32)GSS_S_CONTINUE_NEEDED)
break;
}
maj_stat = gss_display_name(&min_stat, gss_name, &send_tok, &doid);
if (maj_stat != (OM_uint32)GSS_S_COMPLETE) {
g_snprintf(errbuf, sizeof(errbuf),
_("can't display gss name: %s"), gss_error(maj_stat, min_stat));
gss_release_name(&min_stat, &gss_name);
goto out;
}
gss_release_name(&min_stat, &gss_name);
/* get rid of the realm */
if ((p = strchr(send_tok.value, '@')) == NULL) {
g_snprintf(errbuf, sizeof(errbuf),
_("malformed gss name: %s"), (char *)send_tok.value);
amfree(send_tok.value);
goto out;
}
*p = '\0';
realm = ++p;
/*
* If the principal doesn't match, complain
*/
if ((msg = krb5_checkuser(rc->hostname, send_tok.value, realm)) != NULL) {
g_snprintf(errbuf, sizeof(errbuf),
_("access not allowed from %s: %s"), (char *)send_tok.value, msg);
amfree(send_tok.value);
goto out;
}
amfree(send_tok.value);
rval = 0;
out:
set_root_privs(0);
if (rval != 0) {
rc->errmsg = g_strdup(errbuf);
} else {
rc->auth = 1;
}
auth_debug(1, _("gss_server returning %d\n"), rval);
return (rval);
}
/*
* Setup some things about krb5. This should only be called once.
*/
static void
krb5_init(void)
{
static int beenhere = 0;
char *p;
char *myfqhostname=NULL;
if (beenhere)
return;
beenhere = 1;
#ifndef BROKEN_MEMORY_CCACHE
putenv(g_strdup(KRB5_ENV_CCNAME"=MEMORY:amanda_ccache"));
#else
/*
* MEMORY ccaches seem buggy and cause a lot of internal heap
* corruption. malloc has been known to core dump. This behavior
* has been witnessed in Cygnus' kerbnet 1.2, MIT's krb V 1.0.5 and
* MIT's krb V -current as of 3/17/1999.
*
* We just use a lame ccache scheme with a uid suffix.
*/
atexit(cleanup);
{
char *ccache = g_strdup_printf(
KRB5_ENV_CCNAME"=FILE:/tmp/amanda_ccache.%ld.%ld",
(long)geteuid(), (long)getpid());
putenv(ccache);
}
#endif
gethostname(myhostname, sizeof(myhostname) - 1);
myhostname[sizeof(myhostname) - 1] = '\0';
/*
* In case it isn't fully qualified, do a DNS lookup. Ignore
* any errors (this is best-effort).
*/
if (resolve_hostname(myhostname, SOCK_STREAM, NULL, &myfqhostname) == 0
&& myfqhostname != NULL) {
strncpy(myhostname, myfqhostname, sizeof(myhostname)-1);
myhostname[sizeof(myhostname)-1] = '\0';
amfree(myfqhostname);
}
/*
* Lowercase the results. We assume all host/ principals will be
* lowercased.
*/
for (p = myhostname; *p != '\0'; p++) {
if (isupper((int)*p))
*p = tolower(*p);
}
}
#ifdef BROKEN_MEMORY_CCACHE
static void
cleanup(void)
{
#ifdef KDESTROY_VIA_UNLINK
char ccache[64];
g_snprintf(ccache, sizeof(ccache), "/tmp/amanda_ccache.%ld.%ld",
(long)geteuid(), (long)getpid());
unlink(ccache);
#else
kdestroy();
#endif
}
#endif
/*
* Get a ticket granting ticket and stuff it in the cache
*/
static const char *
get_tgt(
char * keytab_name,
char * principal_name)
{
krb5_context context;
krb5_error_code ret;
krb5_principal client = NULL, server = NULL;
krb5_creds creds;
krb5_keytab keytab;
krb5_ccache ccache;
krb5_timestamp now;
#ifdef KRB5_HEIMDAL_INCLUDES
krb5_data tgtname = { KRB5_TGS_NAME_SIZE, KRB5_TGS_NAME };
#else
krb5_data tgtname = { 0, KRB5_TGS_NAME_SIZE, KRB5_TGS_NAME };
#endif
static char *error = NULL;
if (error != NULL) {
amfree(error);
error = NULL;
}
if ((ret = krb5_init_context(&context)) != 0) {
error = g_strdup_printf(_("error initializing krb5 context: %s"),
error_message(ret));
return (error);
}
/*krb5_init_ets(context);*/
if(!keytab_name) {
error = g_strdup(_("error -- no krb5 keytab defined"));
return(error);
}
if(!principal_name) {
error = g_strdup(_("error -- no krb5 principal defined"));
return(error);
}
/*
* Resolve keytab file into a keytab object
*/
if ((ret = krb5_kt_resolve(context, keytab_name, &keytab)) != 0) {
error = g_strdup_printf(_("error resolving keytab %s: %s"), keytab_name,
error_message(ret));
return (error);
}
/*
* Resolve the amanda service held in the keytab into a principal
* object
*/
ret = krb5_parse_name(context, principal_name, &client);
if (ret != 0) {
error = g_strdup_printf(_("error parsing %s: %s"), principal_name,
error_message(ret));
return (error);
}
#ifdef KRB5_HEIMDAL_INCLUDES
ret = krb5_build_principal_ext(context, &server,
krb5_realm_length(*krb5_princ_realm(context, client)),
krb5_realm_data(*krb5_princ_realm(context, client)),
tgtname.length, tgtname.data,
krb5_realm_length(*krb5_princ_realm(context, client)),
krb5_realm_data(*krb5_princ_realm(context, client)),
0);
#else
ret = krb5_build_principal_ext(context, &server,
krb5_princ_realm(context, client)->length,
krb5_princ_realm(context, client)->data,
tgtname.length, tgtname.data,
krb5_princ_realm(context, client)->length,
krb5_princ_realm(context, client)->data,
0);
#endif
if (ret != 0) {
error = g_strdup_printf(_("error while building server name: %s"),
error_message(ret));
return (error);
}
ret = krb5_timeofday(context, &now);
if (ret != 0) {
error = g_strdup_printf(_("error getting time of day: %s"), error_message(ret));
return (error);
}
memset(&creds, 0, sizeof(creds));
creds.times.starttime = 0;
creds.times.endtime = now + AMANDA_TKT_LIFETIME;
creds.client = client;
creds.server = server;
/*
* Get a ticket for the service, using the keytab
*/
ret = krb5_get_in_tkt_with_keytab(context, 0, NULL, NULL, NULL,
keytab, 0, &creds, 0);
if (ret != 0) {
error = g_strdup_printf(_("error getting ticket for %s: %s"),
principal_name, error_message(ret));
goto cleanup2;
}
if ((ret = krb5_cc_default(context, &ccache)) != 0) {
error = g_strdup_printf(_("error initializing ccache: %s"), error_message(ret));
goto cleanup;
}
if ((ret = krb5_cc_initialize(context, ccache, client)) != 0) {
error = g_strdup_printf(_("error initializing ccache: %s"), error_message(ret));
goto cleanup;
}
if ((ret = krb5_cc_store_cred(context, ccache, &creds)) != 0) {
error = g_strdup_printf(_("error storing creds in ccache: %s"),
error_message(ret));
/* FALLTHROUGH */
}
krb5_cc_close(context, ccache);
cleanup:
krb5_free_cred_contents(context, &creds);
cleanup2:
#if 0
krb5_free_principal(context, client);
krb5_free_principal(context, server);
#endif
krb5_free_context(context);
return (error);
}
#ifndef KDESTROY_VIA_UNLINK
/*
* get rid of tickets
*/
static void
kdestroy(void)
{
krb5_context context;
krb5_ccache ccache;
if ((krb5_init_context(&context)) != 0) {
return;
}
if ((krb5_cc_default(context, &ccache)) != 0) {
goto cleanup;
}
krb5_cc_destroy(context, ccache);
krb5_cc_close(context, ccache);
cleanup:
krb5_free_context(context);
return;
}
#endif
/*
* Formats an error from the gss api
*/
static const char *
gss_error(
OM_uint32 major,
OM_uint32 minor)
{
static gss_buffer_desc msg;
OM_uint32 min_stat, msg_ctx;
if (msg.length > 0)
gss_release_buffer(&min_stat, &msg);
msg_ctx = 0;
if (major == GSS_S_FAILURE)
gss_display_status(&min_stat, minor, GSS_C_MECH_CODE, GSS_C_NULL_OID,
&msg_ctx, &msg);
else
gss_display_status(&min_stat, major, GSS_C_GSS_CODE, GSS_C_NULL_OID,
&msg_ctx, &msg);
return ((const char *)msg.value);
}
static int
k5_encrypt(
void *cookie,
void *buf,
ssize_t buflen,
void **encbuf,
ssize_t *encbuflen)
{
struct tcp_conn *rc = cookie;
gss_buffer_desc dectok;
gss_buffer_desc enctok;
OM_uint32 maj_stat, min_stat;
int conf_state;
if (rc->conf_fn && rc->conf_fn("kencrypt", rc->datap)) {
auth_debug(1, _("krb5: k5_encrypt: enter %p\n"), rc);
dectok.length = buflen;
dectok.value = buf;
if (rc->auth == 1) {
assert(rc->gss_context != GSS_C_NO_CONTEXT);
maj_stat = gss_seal(&min_stat, rc->gss_context, 1,
GSS_C_QOP_DEFAULT, &dectok, &conf_state,
&enctok);
if (maj_stat != (OM_uint32)GSS_S_COMPLETE || conf_state == 0) {
auth_debug(1, _("krb5 encrypt error to %s: %s\n"),
rc->hostname, gss_error(maj_stat, min_stat));
return (-1);
}
auth_debug(1, _("krb5: k5_encrypt: give %zu bytes\n"),
enctok.length);
*encbuf = enctok.value;
*encbuflen = enctok.length;
} else {
*encbuf = buf;
*encbuflen = buflen;
}
auth_debug(1, _("krb5: k5_encrypt: exit\n"));
}
return (0);
}
static int
k5_decrypt(
void *cookie,
void *buf,
ssize_t buflen,
void **decbuf,
ssize_t *decbuflen)
{
struct tcp_conn *rc = cookie;
gss_buffer_desc enctok;
gss_buffer_desc dectok;
OM_uint32 maj_stat, min_stat;
int conf_state, qop_state;
if (rc->conf_fn && rc->conf_fn("kencrypt", rc->datap)) {
auth_debug(1, _("krb5: k5_decrypt: enter\n"));
if (rc->auth == 1) {
enctok.length = buflen;
enctok.value = buf;
auth_debug(1, _("krb5: k5_decrypt: decrypting %zu bytes\n"), enctok.length);
assert(rc->gss_context != GSS_C_NO_CONTEXT);
maj_stat = gss_unseal(&min_stat, rc->gss_context, &enctok, &dectok,
&conf_state, &qop_state);
if (maj_stat != (OM_uint32)GSS_S_COMPLETE) {
auth_debug(1, _("krb5 decrypt error from %s: %s\n"),
rc->hostname, gss_error(maj_stat, min_stat));
return (-1);
}
auth_debug(1, _("krb5: k5_decrypt: give %zu bytes\n"),
dectok.length);
*decbuf = dectok.value;
*decbuflen = dectok.length;
} else {
*decbuf = buf;
*decbuflen = buflen;
}
auth_debug(1, _("krb5: k5_decrypt: exit\n"));
} else {
*decbuf = buf;
*decbuflen = buflen;
}
return (0);
}
/*
* check ~/.k5amandahosts to see if this principal is allowed in. If it's
* hardcoded, then we don't check the realm
*/
static char *
krb5_checkuser( char * host,
char * name,
char * realm)
{
#ifdef AMANDA_PRINCIPAL
if(g_str_equal(name, AMANDA_PRINCIPAL)) {
return(NULL);
} else {
return(g_strdup(_("does not match compiled in default")));
}
#else
struct passwd *pwd;
char *ptmp;
char *result = _("generic error"); /* default is to not permit */
FILE *fp = NULL;
struct stat sbuf;
uid_t localuid;
char *line = NULL;
char *filehost = NULL, *fileuser = NULL, *filerealm = NULL;
assert( host != NULL);
assert( name != NULL);
if((pwd = getpwnam(CLIENT_LOGIN)) == NULL) {
result = g_strdup_printf(_("can not find user %s"), CLIENT_LOGIN);
return result;
}
localuid = pwd->pw_uid;
#ifdef USE_AMANDAHOSTS
ptmp = g_strconcat(pwd->pw_dir, "/.k5amandahosts", NULL);
#else
ptmp = g_strconcat(pwd->pw_dir, "/.k5login", NULL);
#endif
if(!ptmp) {
result = g_strdup_printf(_("could not find home directory for %s"), CLIENT_LOGIN);
goto common_exit;
}
auth_debug(1, _("opening ptmp: %s\n"), (ptmp)?ptmp: "NULL!");
if ((fp = fopen(ptmp, "r")) == NULL) {
/* check to see if the ptmp file does nto exist. */
if (errno == ENOENT) {
/*
* in this case we check to see if the principal matches
* the destination user mimicing the .k5login functionality.
*/
if (!g_str_equal(name, CLIENT_LOGIN)) {
result = g_strdup_printf(_("%s does not match %s"),
name, CLIENT_LOGIN);
return result;
}
result = NULL;
goto common_exit;
}
result = g_strdup_printf(_("can not open %s"), ptmp);
return result;
}
auth_debug(1, _("opened ptmp\n"));
if (fstat(fileno(fp), &sbuf) != 0) {
result = g_strdup_printf(_("cannot fstat %s: %s"), ptmp, strerror(errno));
goto common_exit;
}
if (sbuf.st_uid != localuid) {
result = g_strdup_printf(_("%s is owned by %ld, should be %ld"),
ptmp, (long)sbuf.st_uid, (long)localuid);
goto common_exit;
}
if ((sbuf.st_mode & 077) != 0) {
result = g_strdup_printf(
_("%s: incorrect permissions; file must be accessible only by its owner"), ptmp);
goto common_exit;
}
while ((line = pgets(fp)) != NULL) {
if (line[0] == '\0') {
amfree(line);
continue;
}
/* if there's more than one column, then it's the host */
if( (filehost = strtok(line, " \t")) == NULL) {
amfree(line);
continue;
}
/*
* if there's only one entry, then it's a username and we have
* no hostname. (so the principal is allowed from anywhere.
*/
if((fileuser = strtok(NULL, " \t")) == NULL) {
fileuser = filehost;
filehost = NULL;
}
if(filehost && !g_str_equal(filehost, host)) {
amfree(line);
continue;
} else {
auth_debug(1, _("found a host match\n"));
}
if( (filerealm = strchr(fileuser, '@')) != NULL) {
*filerealm++ = '\0';
}
/*
* we have a match. We're going to be a little bit insecure
* and indicate that the principal is correct but the realm is
* not if that's the case. Technically we should say nothing
* and let the user figure it out, but it's helpful for debugging.
* You likely only get this far if you've turned on cross-realm auth
* anyway...
*/
auth_debug(1, _("comparing %s %s\n"), fileuser, name);
if(g_str_equal(fileuser, name)) {
auth_debug(1, _("found a match!\n"));
if(realm && filerealm && (!g_str_equal(realm, filerealm))) {
amfree(line);
continue;
}
result = NULL;
amfree(line);
goto common_exit;
}
amfree(line);
}
result = g_strdup_printf(_("no match in %s"), ptmp);
common_exit:
afclose(fp);
return(result);
#endif /* AMANDA_PRINCIPAL */
}
/*
* return -1 on error
* return 0 on EOF: *handle = H_EOF && *size = 0 if socket closed
* return 0 on EOF: *handle = handle && *size = 0 if stream closed
* return size : *handle = handle && *size = size for data read
*/
static ssize_t
krb5_tcpm_recv_token(
struct tcp_conn *rc,
int * handle,
char ** errmsg,
char ** buf,
ssize_t * size,
int timeout)
{
unsigned int netint[2];
assert(sizeof(netint) == 8);
switch (net_read(rc->read, &netint, sizeof(netint), timeout)) {
case -1:
if (errmsg) {
g_free(*errmsg);
*errmsg = g_strdup_printf(_("recv error: %s"), strerror(errno));
}
auth_debug(1, _("krb5_tcpm_recv_token: A return(-1)\n"));
return (-1);
case 0:
*size = 0;
*handle = H_EOF;
g_free(*errmsg);
*errmsg = g_strdup("SOCKET_EOF");
auth_debug(1, "krb5_tcpm_recv_token: A return(0)\n");
return (0);
default:
break;
}
*size = (ssize_t)ntohl(netint[0]);
*handle = (int)ntohl(netint[1]);
/* amanda protocol packet can be above NETWORK_BLOCK_BYTES */
if (*size > 128*NETWORK_BLOCK_BYTES || *size < 0) {
if (isprint((int)(*size ) & 0xFF) &&
isprint((int)(*size >> 8 ) & 0xFF) &&
isprint((int)(*size >> 16) & 0xFF) &&
isprint((int)(*size >> 24) & 0xFF) &&
isprint((*handle ) & 0xFF) &&
isprint((*handle >> 8 ) & 0xFF) &&
isprint((*handle >> 16) & 0xFF) &&
isprint((*handle >> 24) & 0xFF)) {
char s[101];
int i;
s[0] = ((int)(*size) >> 24) & 0xFF;
s[1] = ((int)(*size) >> 16) & 0xFF;
s[2] = ((int)(*size) >> 8) & 0xFF;
s[3] = ((int)(*size) ) & 0xFF;
s[4] = (*handle >> 24) & 0xFF;
s[5] = (*handle >> 16) & 0xFF;
s[6] = (*handle >> 8 ) & 0xFF;
s[7] = (*handle ) & 0xFF;
i = 8; s[i] = ' ';
while(i<100 && isprint((int)s[i]) && s[i] != '\n') {
switch(net_read(rc->read, &s[i], 1, 0)) {
case -1: s[i] = '\0'; break;
case 0: s[i] = '\0'; break;
default:
dbprintf(_("read: %c\n"), s[i]); i++; s[i]=' ';
break;
}
}
s[i] = '\0';
g_free(*errmsg);
*errmsg = g_strdup_printf(_("krb5_tcpm_recv_token: invalid size: %s"),
s);
dbprintf(_("krb5_tcpm_recv_token: invalid size %s\n"), s);
} else {
g_free(*errmsg);
*errmsg = g_strdup("krb5_tcpm_recv_token: invalid size");
dbprintf("krb5_tcpm_recv_token: invalid size %zd\n", *size);
}
*size = -1;
return -1;
}
amfree(*buf);
*buf = g_malloc((size_t)*size);
if(*size == 0) {
auth_debug(1, "krb5_tcpm_recv_token: read EOF from %d\n", *handle);
g_free(*errmsg);
*errmsg = g_strdup("EOF");
return 0;
}
switch (net_read(rc->read, *buf, (size_t)*size, timeout)) {
case -1:
if (errmsg) {
g_free(*errmsg);
*errmsg = g_strdup_printf(_("recv error: %s"), strerror(errno));
}
auth_debug(1, _("krb5_tcpm_recv_token: B return(-1)\n"));
return (-1);
case 0:
*size = 0;
g_free(*errmsg);
*errmsg = g_strdup("SOCKET_EOF");
auth_debug(1, "krb5_tcpm_recv_token: B return(0)\n");
return (0);
default:
break;
}
auth_debug(1, _("krb5_tcpm_recv_token: read %zd bytes from %d\n"), *size, *handle);
if (*size > 0 && rc->driver->data_decrypt != NULL) {
void *decbuf;
ssize_t decsize;
rc->driver->data_decrypt(rc, *buf, *size, &decbuf, &decsize);
if (*buf != (char *)decbuf) {
amfree(*buf);
*buf = (char *)decbuf;
}
*size = decsize;
}
return((*size));
}