/*
  Copyright (C) 1997-2023 Free Software Foundation, Inc.

  This file is part of GNU Inetutils.

  GNU Inetutils is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or (at
  your option) any later version.

  GNU Inetutils 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
  General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see `http://www.gnu.org/licenses/'. */

/*
 * Copyright (c) 1993
 *	The Regents of the University of California.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/* General-purpose forwarding routines. These routines may be put into */
/* libkrb5.a to allow widespread use */

#include <config.h>

#if defined KRB5 && defined FORWARD
# include <stdio.h>
# include <pwd.h>
# include <netdb.h>

# ifdef HAVE_KRB5_KRB5_H
#  include <krb5/krb5.h>
# endif
# ifdef HAVE_KRB5_ASN1_H
#  include <krb5/asn1.h>
# endif
# ifdef HAVE_KRB5_CRC_32_H
#  include <krb5/crc-32.h>
# endif
# ifdef HAVE_KRB5_LOS_PROTO_H
#  include <krb5/los-proto.h>
# endif
# ifdef HAVE_KRB5_EXT_PROTO_H
#  include <krb5/ext-proto.h>
# endif

# define KRB5_DEFAULT_LIFE 60*60*8	/* 8 hours */
/* helper function: convert flags to necessary KDC options */
# define flags2options(flags) (flags & KDC_TKT_COMMON_MASK)



/* Get a TGT for use at the remote host */
krb5_error_code
get_for_creds (etype, sumtype, rhost, client, enc_key, forwardable, outbuf)
     const krb5_enctype etype;
     const krb5_cksumtype sumtype;
     char *rhost;
     krb5_principal client;
     krb5_keyblock *enc_key;
     int forwardable;		/* Should forwarded TGT also be forwardable? */
     krb5_data *outbuf;
{
  struct hostent *hp;
  krb5_address **addrs;
  krb5_error_code retval;
  krb5_data *scratch;
  krb5_kdc_rep *dec_rep;
  krb5_error *err_reply;
  krb5_response tgsrep;
  krb5_creds creds, tgt;
  krb5_ccache cc;
  krb5_flags kdcoptions;
  krb5_timestamp now;
  char *remote_host;
  char **hrealms;
  int i;

  if (!rhost || !(hp = gethostbyname (rhost)))
    return KRB5_ERR_BAD_HOSTNAME;

  remote_host = (char *) malloc (strlen (hp->h_name) + 1);
  if (!remote_host)
    return ENOMEM;
  strcpy (remote_host, hp->h_name);

  if (retval = krb5_get_host_realm (remote_host, &hrealms))
    {
      free (remote_host);
      return retval;
    }
  if (!hrealms[0])
    {
      free (remote_host);
      krb5_xfree (hrealms);
      return KRB5_ERR_HOST_REALM_UNKNOWN;
    }

  /* Count elements */
  for (i = 0; hp->h_addr_list[i]; i++);

  addrs = (krb5_address **) malloc ((i + 1) * sizeof (*addrs));
  if (!addrs)
    return ENOMEM;

  for (i = 0; hp->h_addr_list[i]; i++)
    {
      addrs[i] = (krb5_address *) malloc (sizeof (krb5_address));
      if (addrs[i])
	{
	  addrs[i]->addrtype = hp->h_addrtype;
	  addrs[i]->length = hp->h_length;
	  addrs[i]->contents = (unsigned char *) malloc (addrs[i]->length);
	  if (!addrs[i]->contents)
	    {
	      krb5_free_addresses (addrs);
	      return ENOMEM;
	    }
	  else
	    memmove ((char *) addrs[i]->contents, hp->h_addr_list[i],
		     addrs[i]->length);
	}
      else
	{
	  return ENOMEM;
	}
    }
  addrs[i] = 0;

  memset ((char *) &creds, 0, sizeof (creds));
  if (retval = krb5_copy_principal (client, &creds.client))
    return retval;

  if (retval = krb5_build_principal_ext (&creds.server,
					 strlen (hrealms[0]),
					 hrealms[0],
					 KRB5_TGS_NAME_SIZE,
					 KRB5_TGS_NAME,
					 client->realm.length,
					 client->realm.data, 0))
    return retval;

  creds.times.starttime = 0;
  if (retval = krb5_timeofday (&now))
    {
      return retval;
    }
  creds.times.endtime = now + KRB5_DEFAULT_LIFE;
  creds.times.renew_till = 0;

  if (retval = krb5_cc_default (&cc))
    {
      return retval;
    }

  /* fetch tgt directly from cache */
  if (retval = krb5_cc_retrieve_cred (cc,
				      KRB5_TC_MATCH_SRV_NAMEONLY,
				      &creds, &tgt))
    {
      return retval;
    }

  /* tgt->client must be equal to creds.client */
  if (!krb5_principal_compare (tgt.client, creds.client))
    return KRB5_PRINC_NOMATCH;

  if (!tgt.ticket.length)
    return (KRB5_NO_TKT_SUPPLIED);

  kdcoptions = flags2options (tgt.ticket_flags) | KDC_OPT_FORWARDED;

  if (!forwardable)		/* Reset KDC_OPT_FORWARDABLE */
    kdcoptions &= ~(KDC_OPT_FORWARDABLE);

  if (retval = krb5_send_tgs (kdcoptions, &creds.times, etype, sumtype, creds.server, addrs, creds.authdata, 0,	/* no padata */
			      0,	/* no second ticket */
			      &tgt, &tgsrep))
    return retval;

# undef cleanup
# define cleanup() free(tgsrep.response.data)

  switch (tgsrep.message_type)
    {
    case KRB5_TGS_REP:
      break;
    case KRB5_ERROR:
    default:
      if (!krb5_is_krb_error (&tgsrep.response))
	{
	  retval = KRB5KRB_AP_ERR_MSG_TYPE;
	}
      else
	retval = decode_krb5_error (&tgsrep.response, &err_reply);
      if (retval)
	{
	  cleanup ();
	  return retval;	/* neither proper reply nor error! */
	}

      retval = err_reply->error + ERROR_TABLE_BASE_krb5;

      krb5_free_error (err_reply);
      cleanup ();
      return retval;
    }
  retval = krb5_decode_kdc_rep (&tgsrep.response, &tgt.keyblock, etype,	/* enctype */
				&dec_rep);

  cleanup ();
  if (retval)
    return retval;
# undef cleanup
# define cleanup() {\
	memset((char *)dec_rep->enc_part2->session->contents, 0,\
	      dec_rep->enc_part2->session->length);\
		  krb5_free_kdc_rep(dec_rep); }

  if (dec_rep->msg_type != KRB5_TGS_REP)
    {
      retval = KRB5KRB_AP_ERR_MSG_TYPE;
      cleanup ();
      return retval;
    }

  /* now it's decrypted and ready for prime time */

  if (!krb5_principal_compare (dec_rep->client, tgt.client))
    {
      cleanup ();
      return KRB5_KDCREP_MODIFIED;
    }

  if (retval = mk_cred (dec_rep, etype, enc_key, 0, 0, outbuf))
    return retval;

  krb5_free_kdc_rep (dec_rep);

  return retval;
# undef cleanup
}



/* Create asn.1 encoded KRB-CRED message from the kdc reply. */
krb5_error_code
mk_cred (dec_rep, etype, key, sender_addr, recv_addr, outbuf)
     krb5_kdc_rep *dec_rep;
     krb5_enctype etype;
     krb5_keyblock *key;
     krb5_address *sender_addr;
     krb5_address *recv_addr;
     krb5_data *outbuf;
{
  krb5_error_code retval;
  krb5_encrypt_block eblock;
  krb5_cred ret_cred;
  krb5_cred_enc_part cred_enc_part;
  krb5_data *scratch;

  if (!valid_etype (etype))
    return KRB5_PROG_ETYPE_NOSUPP;

  ret_cred.tickets = (krb5_ticket **) calloc (2, sizeof (*ret_cred.tickets));
  if (!ret_cred.tickets)
    return ENOMEM;
  ret_cred.tickets[0] = dec_rep->ticket;
  ret_cred.tickets[1] = 0;

  ret_cred.enc_part.etype = etype;
  ret_cred.enc_part.kvno = 0;

  cred_enc_part.ticket_info = (krb5_cred_info **)
    calloc (2, sizeof (*cred_enc_part.ticket_info));
  if (!cred_enc_part.ticket_info)
    {
      krb5_free_tickets (ret_cred.tickets);
      return ENOMEM;
    }
  cred_enc_part.ticket_info[0] = (krb5_cred_info *)
    malloc (sizeof (*cred_enc_part.ticket_info[0]));
  if (!cred_enc_part.ticket_info[0])
    {
      krb5_free_tickets (ret_cred.tickets);
      krb5_free_cred_enc_part (cred_enc_part);
      return ENOMEM;
    }
  cred_enc_part.nonce = 0;

  if (retval = krb5_us_timeofday (&cred_enc_part.timestamp,
				  &cred_enc_part.usec))
    return retval;

  cred_enc_part.s_address = (krb5_address *) sender_addr;
  cred_enc_part.r_address = (krb5_address *) recv_addr;

  cred_enc_part.ticket_info[0]->session = dec_rep->enc_part2->session;
  cred_enc_part.ticket_info[0]->client = dec_rep->client;
  cred_enc_part.ticket_info[0]->server = dec_rep->enc_part2->server;
  cred_enc_part.ticket_info[0]->flags = dec_rep->enc_part2->flags;
  cred_enc_part.ticket_info[0]->times = dec_rep->enc_part2->times;
  cred_enc_part.ticket_info[0]->caddrs = dec_rep->enc_part2->caddrs;

  cred_enc_part.ticket_info[1] = 0;

  /* start by encoding to-be-encrypted part of the message */

  if (retval = encode_krb5_enc_cred_part (&cred_enc_part, &scratch))
    return retval;

# define cleanup_scratch() {  memset(scratch->data, 0, scratch->length); krb5_free_data(scratch); }

  /* put together an eblock for this encryption */

  krb5_use_cstype (&eblock, etype);
  ret_cred.enc_part.ciphertext.length = krb5_encrypt_size (scratch->length,
							   eblock.crypto_entry);
  /* add padding area, and zero it */
  if (!(scratch->data = realloc (scratch->data,
				 ret_cred.enc_part.ciphertext.length)))
    {
      /* may destroy scratch->data */
      krb5_xfree (scratch);
      return ENOMEM;
    }
  memset (scratch->data + scratch->length, 0,
	  ret_cred.enc_part.ciphertext.length - scratch->length);
  if (!(ret_cred.enc_part.ciphertext.data =
	malloc (ret_cred.enc_part.ciphertext.length)))
    {
      retval = ENOMEM;
      goto clean_scratch;
    }

# define cleanup_encpart() {\
	 memset(ret_cred.enc_part.ciphertext.data, 0, \
	     ret_cred.enc_part.ciphertext.length); \
	free(ret_cred.enc_part.ciphertext.data); \
	ret_cred.enc_part.ciphertext.length = 0; \
	ret_cred.enc_part.ciphertext.data = 0;}

  /* do any necessary key pre-processing */
  if (retval = krb5_process_key (&eblock, key))
    {
      goto clean_encpart;
    }

# define cleanup_prockey() { krb5_finish_key(&eblock);}

  /* call the encryption routine */
  if (retval = krb5_encrypt ((krb5_pointer) scratch->data,
			     (krb5_pointer)
			     ret_cred.enc_part.ciphertext.data,
			     scratch->length, &eblock, 0))
    {
      goto clean_prockey;
    }

  /* private message is now assembled-- do some cleanup */
  cleanup_scratch ();

  if (retval = krb5_finish_key (&eblock))
    {
      cleanup_encpart ();
      return retval;
    }
  /* encode private message */
  if (retval = encode_krb5_cred (&ret_cred, &scratch))
    {
      cleanup_encpart ();
      return retval;
    }

  cleanup_encpart ();

  *outbuf = *scratch;
  krb5_xfree (scratch);
  return 0;

clean_prockey:
  cleanup_prockey ();
clean_encpart:
  cleanup_encpart ();
clean_scratch:
  cleanup_scratch ();

  return retval;
# undef cleanup_prockey
# undef cleanup_encpart
# undef cleanup_scratch
}



/* Decode, decrypt and store the forwarded creds in the local ccache. */
krb5_error_code
rd_and_store_for_creds (inbuf, ticket, lusername)
     krb5_data *inbuf;
     krb5_ticket *ticket;
     char *lusername;
{
  krb5_encrypt_block eblock;
  krb5_creds creds;
  krb5_error_code retval;
  char ccname[35];
  krb5_ccache ccache = NULL;
  struct passwd *pwd;

  if (retval = rd_cred (inbuf, ticket->enc_part2->session, &creds, 0, 0))
    {
      return (retval);
    }

  if (!(pwd = (struct passwd *) getpwnam (lusername)))
    {
      return -1;
    }

  sprintf (ccname, "FILE:/tmp/krb5cc_%d", pwd->pw_uid);

  if (retval = krb5_cc_resolve (ccname, &ccache))
    {
      return (retval);
    }

  if (retval = krb5_cc_initialize (ccache, ticket->enc_part2->client))
    {
      return (retval);
    }

  if (retval = krb5_cc_store_cred (ccache, &creds))
    {
      return (retval);
    }

  if (retval = chown (ccname + 5, pwd->pw_uid, -1))
    {
      return (retval);
    }

  return retval;
}



extern krb5_deltat krb5_clockskew;
# define in_clock_skew(date) (abs((date)-currenttime) < krb5_clockskew)

/* Decode the KRB-CRED message, and return creds */
krb5_error_code
rd_cred (inbuf, key, creds, sender_addr, recv_addr)
     const krb5_data *inbuf;
     const krb5_keyblock *key;
     krb5_creds *creds;		/* Filled in */
     const krb5_address *sender_addr;	/* optional */
     const krb5_address *recv_addr;	/* optional */
{
  krb5_error_code retval;
  krb5_encrypt_block eblock;
  krb5_cred *credmsg;
  krb5_cred_enc_part *credmsg_enc_part;
  krb5_data *scratch;
  krb5_timestamp currenttime;

  if (!krb5_is_krb_cred (inbuf))
    return KRB5KRB_AP_ERR_MSG_TYPE;

  /* decode private message */
  if (retval = decode_krb5_cred (inbuf, &credmsg))
    {
      return retval;
    }

# define cleanup_credmsg() {krb5_xfree(credmsg->enc_part.ciphertext.data); krb5_xfree(credmsg);}

  if (!(scratch = (krb5_data *) malloc (sizeof (*scratch))))
    {
      cleanup_credmsg ();
      return ENOMEM;
    }

# define cleanup_scratch() {memset(scratch->data, 0, scratch->length); krb5_xfree(scratch->data);}

  if (retval = encode_krb5_ticket (credmsg->tickets[0], &scratch))
    {
      cleanup_credmsg ();
      cleanup_scratch ();
      return (retval);
    }

  creds->ticket = *scratch;
  if (!(creds->ticket.data = malloc (scratch->length)))
    {
      krb5_xfree (creds->ticket.data);
      return ENOMEM;
    }
  memmove ((char *) creds->ticket.data, (char *) scratch->data,
	   scratch->length);

  cleanup_scratch ();

  if (!valid_etype (credmsg->enc_part.etype))
    {
      cleanup_credmsg ();
      return KRB5_PROG_ETYPE_NOSUPP;
    }

  /* put together an eblock for this decryption */

  krb5_use_cstype (&eblock, credmsg->enc_part.etype);
  scratch->length = credmsg->enc_part.ciphertext.length;

  if (!(scratch->data = malloc (scratch->length)))
    {
      cleanup_credmsg ();
      return ENOMEM;
    }

  /* do any necessary key pre-processing */
  if (retval = krb5_process_key (&eblock, key))
    {
      cleanup_credmsg ();
      cleanup_scratch ();
      return retval;
    }

# define cleanup_prockey() { krb5_finish_key(&eblock);}

  /* call the decryption routine */
  if (retval = krb5_decrypt ((krb5_pointer) credmsg->enc_part.ciphertext.data,
			     (krb5_pointer) scratch->data,
			     scratch->length, &eblock, 0))
    {
      cleanup_credmsg ();
      cleanup_scratch ();
      cleanup_prockey ();
      return retval;
    }

  /* cred message is now decrypted -- do some cleanup */

  cleanup_credmsg ();

  if (retval = krb5_finish_key (&eblock))
    {
      cleanup_scratch ();
      return retval;
    }

  /*  now decode the decrypted stuff */
  if (retval = decode_krb5_enc_cred_part (scratch, &credmsg_enc_part))
    {
      cleanup_scratch ();
      return retval;
    }
  cleanup_scratch ();

# define cleanup_mesg() {krb5_xfree(credmsg_enc_part);}

  if (retval = krb5_timeofday (&currenttime))
    {
      cleanup_mesg ();
      return retval;
    }
  if (!in_clock_skew (credmsg_enc_part->timestamp))
    {
      cleanup_mesg ();
      return KRB5KRB_AP_ERR_SKEW;
    }

  if (sender_addr && credmsg_enc_part->s_address &&
      !krb5_address_compare (sender_addr, credmsg_enc_part->s_address))
    {
      cleanup_mesg ();
      return KRB5KRB_AP_ERR_BADADDR;
    }
  if (recv_addr && credmsg_enc_part->r_address &&
      !krb5_address_compare (recv_addr, credmsg_enc_part->r_address))
    {
      cleanup_mesg ();
      return KRB5KRB_AP_ERR_BADADDR;
    }

  if (credmsg_enc_part->r_address)
    {
      krb5_address **our_addrs;

      if (retval = krb5_os_localaddr (&our_addrs))
	{
	  cleanup_mesg ();
	  return retval;
	}
      if (!krb5_address_search (credmsg_enc_part->r_address, our_addrs))
	{
	  krb5_free_addresses (our_addrs);
	  cleanup_mesg ();
	  return KRB5KRB_AP_ERR_BADADDR;
	}
      krb5_free_addresses (our_addrs);
    }

  if (retval = krb5_copy_principal (credmsg_enc_part->ticket_info[0]->client,
				    &creds->client))
    {
      return (retval);
    }

  if (retval = krb5_copy_principal (credmsg_enc_part->ticket_info[0]->server,
				    &creds->server))
    {
      return (retval);
    }

  if (retval =
      krb5_copy_keyblock_contents (credmsg_enc_part->ticket_info[0]->session,
				   &creds->keyblock))
    {
      return (retval);
    }

# undef clean
# define clean() {\
	memset((char *)creds->keyblock.contents, 0, creds->keyblock.length);}

  creds->times = credmsg_enc_part->ticket_info[0]->times;
  creds->is_skey = FALSE;
  creds->ticket_flags = credmsg_enc_part->ticket_info[0]->flags;

  if (retval = krb5_copy_addresses (credmsg_enc_part->ticket_info[0]->caddrs,
				    &creds->addresses))
    {
      clean ();
      return (retval);
    }

  creds->second_ticket.length = 0;

  creds->authdata = 0;

  cleanup_mesg ();
  return 0;
# undef clean
# undef cleanup_credmsg
# undef cleanup_scratch
# undef cleanup_prockey
# undef cleanup_mesg
}

#endif /* defined(KRB5) && defined(FORWARD) */
