/******************************************************************************/
/* */
/* X r d S e c P r o t o c o l k r b 5 . c c */
/* */
/* (c) 2003 by the Board of Trustees of the Leland Stanford, Jr., University */
/* All Rights Reserved */
/* Produced by Andrew Hanushevsky for Stanford University under contract */
/* DE-AC02-76-SFO0515 with the Department of Energy */
/* Modifications: */
/* - January 2007: add support for forwarded tickets */
/* (author: G. Ganis, CERN) */
/* */
/* This file is part of the XRootD software suite. */
/* */
/* XRootD is free software: you can redistribute it and/or modify it under */
/* the terms of the GNU Lesser General Public License as published by the */
/* Free Software Foundation, either version 3 of the License, or (at your */
/* option) any later version. */
/* */
/* XRootD is distributed in the hope that it will be useful, but WITHOUT */
/* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or */
/* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public */
/* License for more details. */
/* */
/* You should have received a copy of the GNU Lesser General Public License */
/* along with XRootD in a file called COPYING.LESSER (LGPL license) and file */
/* COPYING (GPL license). If not, see . */
/* */
/* The copyright holder's institutional names and contributor's names may not */
/* be used to endorse or promote products derived from this software without */
/* specific prior written permission of the institution or contributor. */
/******************************************************************************/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
extern "C" {
#include "krb5.h"
#ifdef HAVE_ET_COM_ERR_H
#include "et/com_err.h"
#else
#include "com_err.h"
#endif
}
#include "XrdVersion.hh"
#include "XrdNet/XrdNetAddrInfo.hh"
#include "XrdNet/XrdNetUtils.hh"
#include "XrdOuc/XrdOucErrInfo.hh"
#include "XrdOuc/XrdOucEnv.hh"
#include "XrdSys/XrdSysHeaders.hh"
#include "XrdSys/XrdSysPthread.hh"
#include "XrdSys/XrdSysPwd.hh"
#include "XrdOuc/XrdOucTokenizer.hh"
#include "XrdSec/XrdSecInterface.hh"
/******************************************************************************/
/* D e f i n e s */
/******************************************************************************/
#define krb_etxt(x) (char *)error_message(x)
#define XrdSecPROTOIDENT "krb5"
#define XrdSecPROTOIDLEN sizeof(XrdSecPROTOIDENT)
#define XrdSecNOIPCHK 0x0001
#define XrdSecEXPTKN 0x0002
#define XrdSecINITTKN 0x0004
#define XrdSecDEBUG 0x1000
#define XrdSecMAXPATHLEN 4096
#define CLDBG(x) if (client_options & XrdSecDEBUG) cerr <<"Seckrb5: " <= XrdSecMAXPATHLEN) ?
XrdSecMAXPATHLEN -1 : lt;
memcpy(ExpFile, expfile, lt);
ExpFile[lt] = 0;
}
}
XrdSecProtocolkrb5(const char *KP,
const char *hname,
XrdNetAddrInfo &endPoint)
: XrdSecProtocol(XrdSecPROTOIDENT)
{Service = (KP ? strdup(KP) : 0);
Entity.host = strdup(hname);
epAddr = endPoint;
Entity.addrInfo = &epAddr;
CName[0] = '?'; CName[1] = '\0';
Entity.name = CName;
Step = 0;
AuthContext = 0;
AuthClientContext = 0;
Ticket = 0;
Creds = 0;
}
void Delete();
private:
~XrdSecProtocolkrb5() {} // Delete() does it all
static int Fatal(XrdOucErrInfo *erp,int rc,const char *msg1,char *KP=0,int krc=0);
static int get_krbCreds(char *KP, krb5_creds **krb_creds);
void SetAddr(krb5_address &ipadd);
static XrdSysMutex krbContext; // Server
static XrdSysMutex krbClientContext;// Client
static int options; // Server
static int client_options;// Client
static krb5_context krb_context; // Server
static krb5_context krb_client_context; // Client
static krb5_ccache krb_client_ccache; // Client
static krb5_ccache krb_ccache; // Server
static krb5_keytab krb_keytab; // Server
static krb5_principal krb_principal; // Server
static char *Principal; // Server's principal name
static char *Parms; // Server parameters
static char ExpFile[XrdSecMAXPATHLEN]; // Server: (template for)
// file to export token
int exp_krbTkn(XrdSecCredentials *cred, XrdOucErrInfo *erp);
int get_krbFwdCreds(char *KP, krb5_data *outdata);
XrdNetAddrInfo epAddr;
char CName[256]; // Kerberos limit
char *Service; // Target principal for client
char Step; // Indicates at which step we are
krb5_auth_context AuthContext; // Authetication context
krb5_auth_context AuthClientContext; // Authetication context
krb5_ticket *Ticket; // Ticket associated to client authentication
krb5_creds *Creds; // Client: credentials
};
/******************************************************************************/
/* S t a t i c D a t a */
/******************************************************************************/
XrdSysMutex XrdSecProtocolkrb5::krbContext; // Server
XrdSysMutex XrdSecProtocolkrb5::krbClientContext; // Client
int XrdSecProtocolkrb5::client_options = 0;// Client
int XrdSecProtocolkrb5::options = 0; // Server
krb5_context XrdSecProtocolkrb5::krb_context; // Server
krb5_context XrdSecProtocolkrb5::krb_client_context; // Client
krb5_ccache XrdSecProtocolkrb5::krb_client_ccache; // Client
krb5_ccache XrdSecProtocolkrb5::krb_ccache; // Server
krb5_keytab XrdSecProtocolkrb5::krb_keytab = NULL; // Server
krb5_principal XrdSecProtocolkrb5::krb_principal; // Server
char *XrdSecProtocolkrb5::Principal = 0; // Server
char *XrdSecProtocolkrb5::Parms = 0; // Server
char XrdSecProtocolkrb5::ExpFile[XrdSecMAXPATHLEN] = "/tmp/krb5cc_";
/******************************************************************************/
/* D e l e t e */
/******************************************************************************/
void XrdSecProtocolkrb5::Delete()
{
if (Parms) {free(Parms); Parms = 0;}
if (Creds) krb5_free_creds(krb_context, Creds);
if (Ticket) krb5_free_ticket(krb_context, Ticket);
if (AuthContext) krb5_auth_con_free(krb_context, AuthContext);
if (AuthClientContext) krb5_auth_con_free(krb_client_context, AuthClientContext);
if (Entity.host) free(Entity.host);
if (Service) free(Service);
delete this;
}
/******************************************************************************/
/* g e t C r e d e n t i a l s */
/******************************************************************************/
XrdSecCredentials *XrdSecProtocolkrb5::getCredentials(XrdSecParameters *noparm,
XrdOucErrInfo *error)
{
char *buff;
int bsz;
krb_rc rc;
krb5_data outbuf;
CLDBG("getCredentials");
// Supply null credentials if so needed for this protocol
//
if (!Service)
{CLDBG("Null credentials supplied.");
return new XrdSecCredentials(0,0);
}
CLDBG("context lock");
krbClientContext.Lock();
CLDBG("context locked");
// We support passing the credential cache path via Url parameter
//
char *ccn = (error && error->getEnv()) ? error->getEnv()->Get("xrd.k5ccname") : 0;
const char *kccn = ccn ? (const char *)ccn : getenv("KRB5CCNAME");
char ccname[128];
if (!kccn)
{snprintf(ccname, 128, "/tmp/krb5cc_%d", geteuid());
if (access(ccname, R_OK) == 0)
{kccn = ccname;}
}
CLDBG((kccn ? kccn : "credentials cache unset"));
// Initialize the context and get the cache default.
//
if ((rc = krb5_init_context(&krb_client_context)))
{Fatal(error, ENOPROTOOPT, "Kerberos initialization failed", Service, rc);
return (XrdSecCredentials *)0;
}
CLDBG("init context");
// Set the name of the default credentials cache for the Kerberos context
//
if ((rc = krb5_cc_set_default_name(krb_client_context, kccn)))
{Fatal(error, ENOPROTOOPT, "Kerberos default credentials cache setting failed", Service, rc);
return (XrdSecCredentials *)0;
}
CLDBG("cc set default name");
// Obtain the default cache location
//
if ((rc = krb5_cc_default(krb_client_context, &krb_client_ccache)))
{Fatal(error, ENOPROTOOPT, "Unable to locate cred cache", Service, rc);
return (XrdSecCredentials *)0;
}
CLDBG("cc default");
// Check if the server asked for a forwardable ticket
//
char *pfwd = 0;
if ((pfwd = (char *) strstr(Service,",fwd")))
{
client_options |= XrdSecEXPTKN;
*pfwd = 0;
}
// Clear outgoing ticket and lock the kerberos context
//
outbuf.length = 0; outbuf.data = 0;
// If this is not the first call, we are asked to send over a delegated ticket:
// we must create it first
// we save it into a file and return signalling the end of the hand-shake
//
if (Step > 0)
{if ((rc = get_krbFwdCreds(Service, &outbuf)))
{krbClientContext.UnLock();
Fatal(error, ESRCH, "Unable to get forwarded credentials", Service, rc);
return (XrdSecCredentials *)0;
} else
{bsz = XrdSecPROTOIDLEN+outbuf.length;
if (!(buff = (char *)malloc(bsz)))
{krbClientContext.UnLock();
Fatal(error, ENOMEM, "Insufficient memory for credentials.", Service);
return (XrdSecCredentials *)0;
}
strcpy(buff, XrdSecPROTOIDENT);
memcpy((void *)(buff+XrdSecPROTOIDLEN),
(const void *)outbuf.data, (size_t)outbuf.length);
CLDBG("Returned " <ticket_flags & TKT_FLG_FORWARDABLE))
{ if ((client_options & XrdSecINITTKN) && !reinitdone && caninittkn)
{ // Need to re-init
CLPRT("Existing ticket is not forwardable: re-init ");
rc = system(reinitcmd);
CLDBG("getCredentials: return code from '"<size <= (int)XrdSecPROTOIDLEN || !cred->buffer)
{strncpy(Entity.prot, "host", sizeof(Entity.prot));
return 0;
}
// Check if this is a recognized protocol
//
if (strcmp(cred->buffer, XrdSecPROTOIDENT))
{char emsg[256];
snprintf(emsg, sizeof(emsg),
"Authentication protocol id mismatch (%.4s != %.4s).",
XrdSecPROTOIDENT, cred->buffer);
Fatal(error, EINVAL, emsg, Principal);
return -1;
}
CLDBG("protocol check");
char printit[4096];
sprintf(printit,"Step is %d",Step);
CLDBG(printit);
// If this is not the first call the buffer contains a forwarded token:
// we save it into a file and return signalling the end of the hand-shake
//
if (Step > 0)
{if ((rc = exp_krbTkn(cred, error)))
iferror = (char *)"Unable to export the token to file";
if (rc && iferror) {
krbContext.UnLock();
return Fatal(error, EINVAL, iferror, Principal, rc);
}
krbContext.UnLock();
return 0;
}
CLDBG("protocol check");
// Increment the step
//
Step += 1;
// Indicate who we are
//
strncpy(Entity.prot, XrdSecPROTOIDENT, sizeof(Entity.prot));
// Create a kerberos style ticket and obtain the kerberos mutex
//
CLDBG("Context Lock");
inbuf.length = cred->size -XrdSecPROTOIDLEN;
inbuf.data = &cred->buffer[XrdSecPROTOIDLEN];
krbContext.Lock();
// Check if whether the IP address in the credentials must match that of
// the incomming host.
//
CLDBG("Context Locked");
if (!(XrdSecProtocolkrb5::options & XrdSecNOIPCHK))
{SetAddr(ipadd);
iferror = (char *)"Unable to validate ip address;";
if (!(rc=krb5_auth_con_init(krb_context, &AuthContext)))
rc=krb5_auth_con_setaddrs(krb_context, AuthContext, NULL, &ipadd);
}
// Decode the credentials and extract client's name
//
if (!rc)
{if ((rc = krb5_rd_req(krb_context, &AuthContext, &inbuf,
(krb5_const_principal)krb_principal,
krb_keytab, NULL, &Ticket)))
iferror = (char *)"Unable to authenticate credentials;";
else if ((rc = krb5_aname_to_localname(krb_context,
Ticket->enc_part2->client,
sizeof(CName)-1, CName)))
iferror = (char *)"Unable to extract client name;";
}
// Make sure the name is null-terminated
//
CName[sizeof(CName)-1] = '\0';
// If requested, ask the client for a forwardable token
int hsrc = 0;
if (!rc && XrdSecProtocolkrb5::options & XrdSecEXPTKN) {
// We just ask for more; the client knows what to send over
hsrc = 1;
// We need to fill-in a fake buffer
int len = strlen("fwdtgt") + 1;
char *buf = (char *) malloc(len);
memcpy(buf, "fwdtgt", len-1);
buf[len-1] = 0;
*parms = new XrdSecParameters(buf, len);
}
// Release any allocated storage at this point and unlock mutex
//
krbContext.UnLock();
// Diagnose any errors
//
if (rc && iferror)
return Fatal(error, EACCES, iferror, Principal, rc);
// All done
//
return hsrc;
}
/******************************************************************************/
/* I n i t i a l i z a t i o n M e t h o d s */
/******************************************************************************/
/******************************************************************************/
/* I n i t */
/******************************************************************************/
int XrdSecProtocolkrb5::Init(XrdOucErrInfo *erp, char *KP, char *kfn)
{
krb_rc rc;
char buff[2048];
// Create a kerberos context. There is one such context per protocol object.
//
// If we have no principal then this is a client-side call: initializations are done
// in getCredentials to allow for multiple client principals
//
if (!KP) return 0;
if ((rc = krb5_init_context(&krb_context)))
return Fatal(erp, ENOPROTOOPT, "Kerberos initialization failed", KP, rc);
// Obtain the default cache location
//
if ((rc = krb5_cc_default(krb_context, &krb_ccache)))
return Fatal(erp, ENOPROTOOPT, "Unable to locate cred cache", KP, rc);
// Try to resolve the keyfile name
//
if (kfn && *kfn)
{if ((rc = krb5_kt_resolve(krb_context, kfn, &krb_keytab)))
{snprintf(buff, sizeof(buff), "Unable to find keytab '%s';", kfn);
return Fatal(erp, ESRCH, buff, Principal, rc);
}
} else {
krb5_kt_default(krb_context, &krb_keytab);
}
// Keytab name
//
char krb_kt_name[1024];
if ((rc = krb5_kt_get_name(krb_context, krb_keytab, &krb_kt_name[0], 1024)))
{snprintf(buff, sizeof(buff), "Unable to get keytab name;");
return Fatal(erp, ESRCH, buff, Principal, rc);
}
// Check if we can read access the keytab file
//
krb5_kt_cursor ktc;
if ((rc = krb5_kt_start_seq_get(krb_context, krb_keytab, &ktc)))
{snprintf(buff, sizeof(buff), "Unable to start sequence on the keytab file %s", krb_kt_name);
return Fatal(erp, EPERM, buff, Principal, rc);
}
if ((rc = krb5_kt_end_seq_get(krb_context, krb_keytab, &ktc)))
{snprintf(buff, sizeof(buff), "WARNING: unable to end sequence on the keytab file %s", krb_kt_name);
CLPRT(buff);
}
// Now, extract the "principal/instance@realm" from the stream
//
if ((rc = krb5_parse_name(krb_context,KP,&krb_principal)))
return Fatal(erp, EINVAL, "Cannot parse service name", KP, rc);
// Establish the correct principal to use
//
if ((rc = krb5_unparse_name(krb_context,(krb5_const_principal)krb_principal,
(char **)&Principal)))
return Fatal(erp, EINVAL, "Unable to unparse principal;", KP, rc);
// All done
//
return 0;
}
/******************************************************************************/
/* P r i v a t e M e t h o d s */
/******************************************************************************/
/******************************************************************************/
/* F a t a l */
/******************************************************************************/
int XrdSecProtocolkrb5::Fatal(XrdOucErrInfo *erp, int rc, const char *msg,
char *KP, int krc)
{
const char *msgv[8];
int k, i = 0;
msgv[i++] = "Seckrb5: "; //0
msgv[i++] = msg; //1
if (krc) {msgv[i++] = "; "; //2
msgv[i++] = krb_etxt(krc); //3
}
if (KP) {msgv[i++] = " (p="; //4
msgv[i++] = KP; //5
msgv[i++] = ")."; //6
}
if (erp) erp->setErrInfo(rc, msgv, i);
else {for (k = 0; k < i; k++) cerr <");
if (pusr)
{int ln = strlen(CName);
if (ln != 6) {
// Adjust the space
int lm = strlen(ccfile) - (int)(pusr + 6 - &ccfile[0]);
memmove(pusr+ln, pusr+6, lm);
}
// Copy the name
memcpy(pusr, CName, ln);
// Adjust the length
nlen += (ln - 6);
}
char *puid = (char *) strstr(&ccfile[0], "");
struct passwd *pw;
XrdSysPwd thePwd(CName, &pw);
if (puid)
{char cuid[20] = {0};
if (pw)
sprintf(cuid, "%d", pw->pw_uid);
int ln = strlen(cuid);
if (ln != 5) {
// Adjust the space
int lm = strlen(ccfile) - (int)(puid + 5 - &ccfile[0]);
memmove(puid+ln, pusr+5, lm);
}
// Copy the name
memcpy(puid, cuid, ln);
// Adjust the length
nlen += (ln - 5);
}
// Terminate to the new length
//
ccfile[nlen] = 0;
// Point the received creds
//
krbContext.Lock();
krb5_data forwardCreds;
forwardCreds.data = &cred->buffer[XrdSecPROTOIDLEN];
forwardCreds.length = cred->size -XrdSecPROTOIDLEN;
// Get the replay cache
//
krb5_rcache rcache;
if ((rc = krb5_get_server_rcache(krb_context,
krb5_princ_component(krb_context, krb_principal, 0),
&rcache)))
return rc;
if ((rc = krb5_auth_con_setrcache(krb_context, AuthContext, rcache)))
return rc;
// Fill-in remote address
//
SetAddr(ipadd);
if ((rc = krb5_auth_con_setaddrs(krb_context, AuthContext, 0, &ipadd)))
return rc;
// Readout the credentials
//
krb5_creds **creds = 0;
if ((rc = krb5_rd_cred(krb_context, AuthContext,
&forwardCreds, &creds, 0)))
return rc;
// Resolve cache name
krb5_ccache cache = 0;
if ((rc = krb5_cc_resolve(krb_context, ccfile, &cache)))
return rc;
// Init cache
//
if ((rc = krb5_cc_initialize(krb_context, cache,
Ticket->enc_part2->client)))
return rc;
// Store credentials in cache
//
if ((rc = krb5_cc_store_cred(krb_context, cache, *creds)))
return rc;
// Close cache
if ((rc = krb5_cc_close(krb_context, cache)))
return rc;
// Change permission and ownership of the file
//
if (chmod(ccfile, 0600) == -1)
return Fatal(erp, errno, "Unable to change file permissions;", ccfile, 0);
// Done
//
return 0;
}
/******************************************************************************/
/* S e t A d d r */
/******************************************************************************/
void XrdSecProtocolkrb5::SetAddr(krb5_address &ipadd)
{
// The below is a hack but that's how it is actually done!
//
if (epAddr.Family() == AF_INET6)
{struct sockaddr_in6 *ip = (struct sockaddr_in6 *)epAddr.SockAddr();
ipadd.addrtype = ADDRTYPE_INET6;
ipadd.length = sizeof(ip->sin6_addr);
ipadd.contents = (krb5_octet *)&ip->sin6_addr;
} else {
struct sockaddr_in *ip = (struct sockaddr_in *)epAddr.SockAddr();
ipadd.addrtype = ADDRTYPE_INET;
ipadd.length = sizeof(ip->sin_addr);
ipadd.contents = (krb5_octet *)&ip->sin_addr;
}
}
/******************************************************************************/
/* X r d S e c p r o t o c o l k r b 5 I n i t */
/******************************************************************************/
extern "C"
{
char *XrdSecProtocolkrb5Init(const char mode,
const char *parms,
XrdOucErrInfo *erp)
{
char *op, *KPrincipal=0, *Keytab=0, *ExpFile=0;
char parmbuff[1024];
XrdOucTokenizer inParms(parmbuff);
int options = XrdSecNOIPCHK;
static bool serverinitialized = false;
// For client-side one-time initialization, we only need to set debug flag and
// initialize the kerberos context and cache location.
//
if ((mode == 'c') || (serverinitialized))
{
int opts = 0;
if (getenv("XrdSecDEBUG")) opts |= XrdSecDEBUG;
if (getenv("XrdSecKRB5INITTKN")) opts |= XrdSecINITTKN;
XrdSecProtocolkrb5::setClientOpts(opts);
return (XrdSecProtocolkrb5::Init(erp) ? (char *)0 : (char *)"");
}
if (!serverinitialized) {
serverinitialized = true;
}
// Duplicate the parms
//
if (parms) strlcpy(parmbuff, parms, sizeof(parmbuff));
else {char *msg = (char *)"Seckrb5: Kerberos parameters not specified.";
if (erp) erp->setErrInfo(EINVAL, msg);
else cerr <] [-ipchk] [-exptkn[:filetemplate]]
//
if (inParms.GetLine())
{if ((op = inParms.GetToken()) && *op == '/')
{Keytab = op; op = inParms.GetToken();}
if (op && !strcmp(op, "-ipchk"))
{options &= ~XrdSecNOIPCHK;
op = inParms.GetToken();
}
if (op && !strncmp(op, "-exptkn", 7))
{options |= XrdSecEXPTKN;
if (op[7] == ':') ExpFile = op+8;
op = inParms.GetToken();
}
KPrincipal = strdup(op);
}
if (ExpFile)
fprintf(stderr,"Template for exports: %s\n", ExpFile);
else
fprintf(stderr,"Template for exports not set\n");
// Now make sure that we have all the right info
//
if (!KPrincipal)
{char *msg = (char *)"Seckrb5: Kerberos principal not specified.";
if (erp) erp->setErrInfo(EINVAL, msg);
else cerr <");
char *phost = (char *) strstr(&KPrincipal[0], "");
if (phost)
{char *hn = XrdNetUtils::MyHostName();
if (hn)
{int lhn = strlen(hn);
if (lhn != lkey) {
// Allocate, if needed
int lnew = plen - lkey + lhn;
if (lnew > plen) {
KPrincipal = (char *) realloc(KPrincipal, lnew+1);
KPrincipal[lnew] = 0;
phost = (char *) strstr(&KPrincipal[0], "");
}
// Adjust the space
int lm = plen - (int)(phost + lkey - &KPrincipal[0]);
memmove(phost + lhn, phost + lkey, lm);
}
// Copy the name
memcpy(phost, hn, lhn);
// Cleanup
free(hn);
}
}
// Now initialize the server
//
options |= XrdSecDEBUG;
XrdSecProtocolkrb5::setExpFile(ExpFile);
XrdSecProtocolkrb5::setOpts(options);
if (!XrdSecProtocolkrb5::Init(erp, KPrincipal, Keytab))
{free(KPrincipal);
int lpars = strlen(XrdSecProtocolkrb5::getPrincipal());
if (options & XrdSecEXPTKN)
lpars += strlen(",fwd");
char *params = (char *)malloc(lpars+1);
if (params)
{memset(params,0,lpars+1);
strcpy(params,XrdSecProtocolkrb5::getPrincipal());
if (options & XrdSecEXPTKN)
strcat(params,",fwd");
XrdSecProtocolkrb5::setParms(params);
return params;
}
return (char *)0;
}
// Failure
//
free(KPrincipal);
return (char *)0;
}
}
/******************************************************************************/
/* X r d S e c P r o t o c o l k r b 5 O b j e c t */
/******************************************************************************/
extern "C"
{
XrdSecProtocol *XrdSecProtocolkrb5Object(const char mode,
const char *hostname,
XrdNetAddrInfo &endPoint,
const char *parms,
XrdOucErrInfo *erp)
{
XrdSecProtocolkrb5 *prot;
char *KPrincipal=0;
// If this is a client call, then we need to get the target principal from the
// parms (which must be the first and only token). For servers, we use the
// context we established at initialization time.
//
if (mode == 'c')
{if ((KPrincipal = (char *)parms)) while(*KPrincipal == ' ') KPrincipal++;
if (!KPrincipal || !*KPrincipal)
{char *msg = (char *)"Seckrb5: Kerberos principal not specified.";
if (erp) erp->setErrInfo(EINVAL, msg);
else cerr <setErrInfo(ENOMEM, msg);
else cerr <