/* ========================================================================
 * Copyright 1988-2006 University of Washington
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * 
 * ========================================================================
 */

/*
 * Program:	GSSAPI authenticator
 *
 * Author:	Mark Crispin
 *		Networks and Distributed Computing
 *		Computing & Communications
 *		University of Washington
 *		Administration Building, AG-44
 *		Seattle, WA  98195
 *		Internet: MRC@CAC.Washington.EDU
 *
 * Date:	12 January 1998
 * Last Edited:	30 August 2006
 */


long auth_gssapi_valid (void);
long auth_gssapi_client (authchallenge_t challenger,authrespond_t responder,
			 char *service,NETMBX *mb,void *stream,
			 unsigned long *trial,char *user);
long auth_gssapi_client_work (authchallenge_t challenger,gss_buffer_desc chal,
			      authrespond_t responder,char *service,NETMBX *mb,
			      void *stream,char *user,kinit_t ki);
char *auth_gssapi_server (authresponse_t responder,int argc,char *argv[]);


AUTHENTICATOR auth_gss = {
  AU_SECURE | AU_AUTHUSER,	/* secure authenticator */
  "GSSAPI",			/* authenticator name */
  auth_gssapi_valid,		/* check if valid */
  auth_gssapi_client,		/* client method */
  auth_gssapi_server,		/* server method */
  NIL				/* next authenticator */
};

#define AUTH_GSSAPI_P_NONE 1
#define AUTH_GSSAPI_P_INTEGRITY 2
#define AUTH_GSSAPI_P_PRIVACY 4

#define AUTH_GSSAPI_C_MAXSIZE 8192

#define SERVER_LOG(x,y) syslog (LOG_ALERT,x,y)

/* Check if GSSAPI valid on this system
 * Returns: T if valid, NIL otherwise
 */

long auth_gssapi_valid (void)
{
  char tmp[MAILTMPLEN];
  OM_uint32 smn;
  gss_buffer_desc buf;
  gss_name_t name;
				/* make service name */
  sprintf (tmp,"%s@%s",(char *) mail_parameters (NIL,GET_SERVICENAME,NIL),
	   mylocalhost ());
  buf.length = strlen (buf.value = tmp);
				/* see if can build a name */
  if (gss_import_name (&smn,&buf,GSS_C_NT_HOSTBASED_SERVICE,&name) !=
      GSS_S_COMPLETE) return NIL;
				/* remove server method if no keytab */
  if (!kerberos_server_valid ()) auth_gss.server = NIL;
  gss_release_name (&smn,&name);/* finished with name */
  return LONGT;
}

/* Client authenticator
 * Accepts: challenger function
 *	    responder function
 *	    SASL service name
 *	    parsed network mailbox structure
 *	    stream argument for functions
 *	    pointer to current trial count
 *	    returned user name
 * Returns: T if success, NIL otherwise, number of trials incremented if retry
 */

long auth_gssapi_client (authchallenge_t challenger,authrespond_t responder,
			 char *service,NETMBX *mb,void *stream,
			 unsigned long *trial,char *user)
{
  gss_buffer_desc chal;
  kinit_t ki = (kinit_t) mail_parameters (NIL,GET_KINIT,NIL);
  long ret = NIL;
  *trial = 65535;		/* never retry */
				/* get initial (empty) challenge */
  if ((chal.value = (*challenger) (stream,(unsigned long *) &chal.length)) != NULL) {
    if (chal.length) {		/* abort if challenge non-empty */
      mm_log ("Server bug: non-empty initial GSSAPI challenge",WARN);
      (*responder) (stream,NIL,0);
      ret = LONGT;		/* will get a BAD response back */
    }
    else if (mb->authuser[0] && strcmp (mb->authuser,myusername ())) {
      mm_log ("Can't use Kerberos: invalid /authuser",WARN);
      (*responder) (stream,NIL,0);
      ret = LONGT;		/* will get a BAD response back */
    }
    else ret = auth_gssapi_client_work (challenger,chal,responder,service,mb,
					stream,user,ki);
  }
  return ret;
}

/* Client authenticator worker function
 * Accepts: challenger function
 *	    responder function
 *	    SASL service name
 *	    parsed network mailbox structure
 *	    stream argument for functions
 *	    returned user name
 *	    kinit function pointer if should retry with kinit
 * Returns: T if success, NIL otherwise
 */

long auth_gssapi_client_work (authchallenge_t challenger,gss_buffer_desc chal,
			      authrespond_t responder,char *service,NETMBX *mb,
			      void *stream,char *user,kinit_t ki)
{
  char tmp[MAILTMPLEN];
  OM_uint32 smj,smn,dsmj,dsmn;
  OM_uint32 mctx = 0;
  gss_ctx_id_t ctx = GSS_C_NO_CONTEXT;
  gss_buffer_desc resp,buf;
  long i;
  int conf;
  gss_qop_t qop;
  gss_name_t crname = NIL;
  blocknotify_t bn = (blocknotify_t) mail_parameters (NIL,GET_BLOCKNOTIFY,NIL);
  void *data;
  long ret = NIL;
  sprintf (tmp,"%s@%s",service,mb->host);
  buf.length = strlen (buf.value = tmp);
				/* get service name */
  if (gss_import_name (&smn,&buf,GSS_C_NT_HOSTBASED_SERVICE,&crname) !=
       GSS_S_COMPLETE) {
    mm_log ("Can't import Kerberos service name",WARN);
    (*responder) (stream,NIL,0);
  }
  else {
    data = (*bn) (BLOCK_SENSITIVE,NIL);
				/* negotiate with KDC */
    smj = gss_init_sec_context (&smn,GSS_C_NO_CREDENTIAL,&ctx,crname,NIL,
				GSS_C_INTEG_FLAG | GSS_C_MUTUAL_FLAG |
				GSS_C_REPLAY_FLAG,0,GSS_C_NO_CHANNEL_BINDINGS,
				GSS_C_NO_BUFFER,NIL,&resp,NIL,NIL);
    (*bn) (BLOCK_NONSENSITIVE,data);

				/* while continuation needed */
    while (smj == GSS_S_CONTINUE_NEEDED) {
      if (chal.value) fs_give ((void **) &chal.value);
				/* send response, get next challenge */
      i = (*responder) (stream,resp.value,resp.length) &&
	(chal.value = (*challenger) (stream,(unsigned long *) &chal.length));
      gss_release_buffer (&smn,&resp);
      if (i) {			/* negotiate continuation with KDC */
	data = (*bn) (BLOCK_SENSITIVE,NIL);
	switch (smj =		/* make sure continuation going OK */
		gss_init_sec_context (&smn,GSS_C_NO_CREDENTIAL,&ctx,
				      crname,GSS_C_NO_OID,GSS_C_INTEG_FLAG |
				      GSS_C_MUTUAL_FLAG | GSS_C_REPLAY_FLAG,0,
				      GSS_C_NO_CHANNEL_BINDINGS,&chal,NIL,
				      &resp,NIL,NIL)) {
	case GSS_S_CONTINUE_NEEDED:
	case GSS_S_COMPLETE:
	  break;
	default:		/* error, don't need context any more */
	  gss_delete_sec_context (&smn,&ctx,NIL);
	}
	(*bn) (BLOCK_NONSENSITIVE,data);
      }
      else {			/* error in continuation */
	mm_log ("Error in negotiating Kerberos continuation",WARN);
	(*responder) (stream,NIL,0);
				/* don't need context any more */
	gss_delete_sec_context (&smn,&ctx,NIL);
	break;
      }
    }

    switch (smj) {		/* done - deal with final condition */
    case GSS_S_COMPLETE:
      if (chal.value) fs_give ((void **) &chal.value);
				/* get prot mechanisms and max size */
      if ((*responder) (stream,resp.value ? resp.value : "",resp.length) &&
	  (chal.value = (*challenger) (stream,(unsigned long *)&chal.length))&&
	  (gss_unwrap (&smn,ctx,&chal,&resp,&conf,&qop) == GSS_S_COMPLETE) &&
	  (resp.length >= 4) && (*((char *) resp.value) & AUTH_GSSAPI_P_NONE)){
				/* make copy of flags and length */
	memcpy (tmp,resp.value,4);
	gss_release_buffer (&smn,&resp);
				/* no session protection */
	tmp[0] = AUTH_GSSAPI_P_NONE;
				/* install user name */
	strcpy (tmp+4,strcpy (user,mb->user[0] ? mb->user : myusername ()));
	buf.value = tmp; buf.length = strlen (user) + 4;
				/* successful negotiation */
	switch (smj = gss_wrap (&smn,ctx,NIL,qop,&buf,&conf,&resp)) {
	case GSS_S_COMPLETE:
	  if ((*responder) (stream,resp.value,resp.length)) ret = T;
	  gss_release_buffer (&smn,&resp);
	  break;
	default:
	  do switch (dsmj = gss_display_status (&dsmn,smj,GSS_C_GSS_CODE,
						GSS_C_NO_OID,&mctx,&resp)) {
	  case GSS_S_COMPLETE:
	    mctx = 0;
	  case GSS_S_CONTINUE_NEEDED:
	    sprintf (tmp,"Unknown gss_wrap failure: %s",(char *) resp.value);
	    mm_log (tmp,WARN);
	    gss_release_buffer (&dsmn,&resp);
	  }
	  while (dsmj == GSS_S_CONTINUE_NEEDED);
	  do switch (dsmj = gss_display_status (&dsmn,smn,GSS_C_MECH_CODE,
						GSS_C_NO_OID,&mctx,&resp)) {
	  case GSS_S_COMPLETE:
	  case GSS_S_CONTINUE_NEEDED:
	    sprintf (tmp,"GSSAPI mechanism status: %s",(char *) resp.value);
	    mm_log (tmp,WARN);
	    gss_release_buffer (&dsmn,&resp);
	  }
	  while (dsmj == GSS_S_CONTINUE_NEEDED);
	  (*responder) (stream,NIL,0);
	}
      }
				/* flush final challenge */
      if (chal.value) fs_give ((void **) &chal.value);
				/* don't need context any more */
      gss_delete_sec_context (&smn,&ctx,NIL);
      break;

    case GSS_S_CREDENTIALS_EXPIRED:
      if (chal.value) fs_give ((void **) &chal.value);
				/* retry if application kinits */
      if (ki && (*ki) (mb->host,"Kerberos credentials expired"))
	ret = auth_gssapi_client_work (challenger,chal,responder,service,mb,
				       stream,user,NIL);
      else {			/* application can't kinit */
	sprintf (tmp,"Kerberos credentials expired (try running kinit) for %s",
		 mb->host);
	mm_log (tmp,WARN);
	(*responder) (stream,NIL,0);
      }
      break;
    case GSS_S_FAILURE:
      if (chal.value) fs_give ((void **) &chal.value);
      do switch (dsmj = gss_display_status (&dsmn,smn,GSS_C_MECH_CODE,
					    GSS_C_NO_OID,&mctx,&resp)) {
      case GSS_S_COMPLETE:	/* end of message, can kinit? */
	if (ki && kerberos_try_kinit (smn) &&
	    (*ki) (mb->host,(char *) resp.value)) {
	  gss_release_buffer (&dsmn,&resp);
	  ret = auth_gssapi_client_work (challenger,chal,responder,service,mb,
					 stream,user,NIL);
	  break;		/* done */
	}
	else (*responder) (stream,NIL,0);
      case GSS_S_CONTINUE_NEEDED:
	sprintf (tmp,kerberos_try_kinit (smn) ?
		 "Kerberos error: %.80s (try running kinit) for %.80s" :
		 "GSSAPI failure: %s for %.80s",(char *) resp.value,mb->host);
	mm_log (tmp,WARN);
	gss_release_buffer (&dsmn,&resp);
      } while (dsmj == GSS_S_CONTINUE_NEEDED);
      break;

    default:			/* miscellaneous errors */
      if (chal.value) fs_give ((void **) &chal.value);
      do switch (dsmj = gss_display_status (&dsmn,smj,GSS_C_GSS_CODE,
					    GSS_C_NO_OID,&mctx,&resp)) {
      case GSS_S_COMPLETE:
	mctx = 0;
      case GSS_S_CONTINUE_NEEDED:
	sprintf (tmp,"Unknown GSSAPI failure: %s",(char *) resp.value);
	mm_log (tmp,WARN);
	gss_release_buffer (&dsmn,&resp);
      }
      while (dsmj == GSS_S_CONTINUE_NEEDED);
      do switch (dsmj = gss_display_status (&dsmn,smn,GSS_C_MECH_CODE,
					    GSS_C_NO_OID,&mctx,&resp)) {
      case GSS_S_COMPLETE:
      case GSS_S_CONTINUE_NEEDED:
	sprintf (tmp,"GSSAPI mechanism status: %s",(char *) resp.value);
	mm_log (tmp,WARN);
	gss_release_buffer (&dsmn,&resp);
      }
      while (dsmj == GSS_S_CONTINUE_NEEDED);
      (*responder) (stream,NIL,0);
      break;
    }
				/* finished with credentials name */
    if (crname) gss_release_name (&smn,&crname);
  }
  return ret;			/* return status */
}

/* Server authenticator
 * Accepts: responder function
 *	    argument count
 *	    argument vector
 * Returns: authenticated user name or NIL
 */

char *auth_gssapi_server (authresponse_t responder,int argc,char *argv[])
{
  char *ret = NIL;
  char tmp[MAILTMPLEN];
  unsigned long maxsize = htonl (AUTH_GSSAPI_C_MAXSIZE);
  int conf;
  OM_uint32 smj,smn,dsmj,dsmn,flags;
  OM_uint32 mctx = 0;
  gss_name_t crname,name;
  gss_OID mech;
  gss_buffer_desc chal,resp,buf;
  gss_cred_id_t crd;
  gss_ctx_id_t ctx = GSS_C_NO_CONTEXT;
  gss_qop_t qop = GSS_C_QOP_DEFAULT;
				/* make service name */
  sprintf (tmp,"%s@%s",(char *) mail_parameters (NIL,GET_SERVICENAME,NIL),
	   tcp_serverhost ());
  buf.length = strlen (buf.value = tmp);
				/* acquire credentials */
  if ((gss_import_name (&smn,&buf,GSS_C_NT_HOSTBASED_SERVICE,&crname)) ==
      GSS_S_COMPLETE) {
    if ((smj = gss_acquire_cred (&smn,crname,0,NIL,GSS_C_ACCEPT,&crd,NIL,NIL))
	== GSS_S_COMPLETE) {
      if ((resp.value = (*responder) ("",0,(unsigned long *) &resp.length)) != NULL) {
	do {			/* negotiate authentication */
	  smj = gss_accept_sec_context (&smn,&ctx,crd,&resp,
					GSS_C_NO_CHANNEL_BINDINGS,&name,&mech,
					&chal,&flags,NIL,NIL);
				/* don't need response any more */
	  fs_give ((void **) &resp.value);
	  switch (smj) {	/* how did it go? */
	  case GSS_S_COMPLETE:	/* successful */
	  case GSS_S_CONTINUE_NEEDED:
	    if (chal.value) {	/* send challenge, get next response */
	      resp.value = (*responder) (chal.value,chal.length,
					 (unsigned long *) &resp.length);
	      gss_release_buffer (&smn,&chal);
	    }
	    break;
	  }
	}
	while (resp.value && resp.length && (smj == GSS_S_CONTINUE_NEEDED));

				/* successful exchange? */
	if ((smj == GSS_S_COMPLETE) &&
	    (gss_display_name (&smn,name,&buf,&mech) == GSS_S_COMPLETE)) {
				/* send security and size */
	  memcpy (resp.value = tmp,(void *) &maxsize,resp.length = 4);
	  tmp[0] = AUTH_GSSAPI_P_NONE;
	  if (gss_wrap (&smn,ctx,NIL,qop,&resp,&conf,&chal) == GSS_S_COMPLETE){
	    resp.value = (*responder) (chal.value,chal.length,
				       (unsigned long *) &resp.length);
	    gss_release_buffer (&smn,&chal);
	    if (gss_unwrap (&smn,ctx,&resp,&chal,&conf,&qop)==GSS_S_COMPLETE) {
				/* client request valid */
	      if (chal.value && (chal.length > 4) &&
		  (chal.length < (MAILTMPLEN - 1)) &&
		  memcpy (tmp,chal.value,chal.length) &&
		  (tmp[0] & AUTH_GSSAPI_P_NONE)) {
				/* tie off authorization ID */
		tmp[chal.length] = '\0';
		ret = kerberos_login (tmp+4,buf.value,argc,argv);
	      }
				/* done with user name */
	      gss_release_buffer (&smn,&chal);
	    }
				/* finished with response */
	    fs_give ((void **) &resp.value);
	  }
				/* don't need name buffer any more */
	  gss_release_buffer (&smn,&buf);
	}
				/* don't need client name any more */
	gss_release_name (&smn,&name);
				/* don't need context any more */
	if (ctx != GSS_C_NO_CONTEXT) gss_delete_sec_context (&smn,&ctx,NIL);
      }
				/* finished with credentials */
      gss_release_cred (&smn,&crd);
    }

    else {			/* can't acquire credentials! */
      if (gss_display_name (&dsmn,crname,&buf,&mech) == GSS_S_COMPLETE)
	SERVER_LOG ("Failed to acquire credentials for %s",buf.value);
      if (smj != GSS_S_FAILURE) do
	switch (dsmj = gss_display_status (&dsmn,smj,GSS_C_GSS_CODE,
					   GSS_C_NO_OID,&mctx,&resp)) {
	case GSS_S_COMPLETE:
	  mctx = 0;
	case GSS_S_CONTINUE_NEEDED:
	  SERVER_LOG ("Unknown GSSAPI failure: %s",resp.value);
	  gss_release_buffer (&dsmn,&resp);
	}
      while (dsmj == GSS_S_CONTINUE_NEEDED);
      do switch (dsmj = gss_display_status (&dsmn,smn,GSS_C_MECH_CODE,
					    GSS_C_NO_OID,&mctx,&resp)) {
      case GSS_S_COMPLETE:
      case GSS_S_CONTINUE_NEEDED:
	SERVER_LOG ("GSSAPI mechanism status: %s",resp.value);
	gss_release_buffer (&dsmn,&resp);
      }
      while (dsmj == GSS_S_CONTINUE_NEEDED);
    }
				/* finished with credentials name */
    gss_release_name (&smn,&crname);
  }
  return ret;			/* return status */
}
