/* verify_key.c --
 *
 * this file contains the code to verify and sign a PGP key.  The main
 * function, verify_and_sign_key() will take an input key, length, 
 * output space, and length, and then try to verify and sign it.
 *
 * Created by:	Derek Atkins <warlord@MIT.EDU>
 *
 * Copyright 1994 Derek A. Atkins and the Massachusetts Institute of
 * Technology
 *
 * For copying and distribution information, please see the file
 * <warlord-copyright.h>.
 *
 * $Source: /mit/warlord/C/pgpsign/src/RCS/verify_key.c,v $
 * $Author: warlord $
 *
 */

#include "warlord-copyright.h"
#include "pgpsign.h"
#include "pgpglue.h"

static char disallowed_chars[] = "`~#$^&*()=[]{};'\"\\|,<>/?";

/* this function is passed in the information from the key-add
 * request, and it will then compare the public key information
 * from that to the kerberos authentication, and return 0 if 
 * it is a valid match.  Fill in the username with the appropriate
 * username to sign
 */
static int
verify_key(FILE *input, AUTH_DAT *auth_dat, char **username, char **error_msg)
{
  char buffer[BUFSIZ];
  int state = 0;
  char *userp, *realmp, *nameend;

  /* Check Args */
  assert (input != NULL);
  assert (auth_dat != NULL);
  assert (username != NULL);
  assert (error_msg != NULL);

  *error_msg = NULL;

  /* First, make sure we are using the null instance. */
  if (*auth_dat->pinst != '\0') {
    syslog(LOG_ERR, "Used non-null instance, %s.%s@%s", auth_dat->pname,
	   auth_dat->pinst, auth_dat->prealm);
    *error_msg = "Invalid Kerberos(TM) Principal:  non_null instance used";
    return -1;
  }

  /* Then, compare the authenticator realm to the valid list of
   * Kerberos realms.  If this fails, then we should ignore
   * this request totally
   */
  if (!string_in_list(auth_dat->prealm, krb_realm_list(), 0)) {
    syslog(LOG_ERR, "Realm %s does not match a valid realm.");
    *error_msg = "Invalid Kerberos(TM) Realm:  does not match valid realm";
    return -1;
  }

  while(fgets(buffer, BUFSIZ, input) != NULL) {
    /* Read in, line-by-line, until we get the line that
     * starts "pub", since that is going to be the keyname.
     */

    switch(state) {

    case 0:
      /* Wait for new keys.. */
      if (strncmp(buffer, "Looking for new keys", 20))
	continue;

      state = 1;
      break;

    case 1:
      if (!strncmp(buffer, "Checking signatures", 19)) {
	/* If we've gotten this far, we're in trouble 
	 * just return, since we have no luck in finding the 
	 * key.
	 */
	syslog(LOG_INFO, "Key not found.");
	if (*error_msg == NULL)
	  *error_msg = "No valid keys found in data.";
	return -1;
      }

      if (strncmp(buffer, "pub", 3))
	continue;

      syslog(LOG_DEBUG, "Got what I think is a public key.");

      /* If we've gotten here, then this is a public key; let's
       * compare the name...
       */
      if ((userp = strchr(buffer, '<')) == NULL) {
	/* No open-brace, cannot find the username */
	*error_msg = "Cannot locate username in key user ID:  requires <user@host>";
	continue;
      }

      /* Make sure it is the *only* '<' character */
      if (userp != strrchr(buffer, '<')) {
	*error_msg = "More than one name in key user ID:  requires <user@host>";
	continue;
      }	

      userp++;
      /* userp should now be pointing to the beginning of the
       * username..  Let's check!
       */
      if (strncasecmp(userp, auth_dat->pname, strlen(auth_dat->pname))) {
	/* Oops, its not! Continue */
	*error_msg = "Username in key does not match Kerberos(TM) Principal";
	continue;
      }

      /* Great, it seems to match.  Let's do a little further checking. 
       * Find the realm and compare that!
       */
      realmp = userp + strlen(auth_dat->pname);
      if (*realmp != '@') {
	/* Huh?  Someone is trying to trick us!  The username in the
	 * key starts the same but is longer than the kerberos name!
	 */
	*error_msg = "Mail host not found in key user ID:  requires <user@host>";
	continue;
      }

      realmp++;			/* Now point at KeyID mail realm */

      if ((nameend = strchr(realmp, '>')) == NULL) {
	/* Hmm, no close-brace.. Die. */
	*error_msg = "Cannot locate end-of-name in key user ID:  requires <user@host>";
	continue;
      }

      /* make sure this is the only ending marker as well... */
      if (nameend != strrchr(realmp, '>')) {
	*error_msg = "Found extra ending markers in user ID:  requires <user@host>";
	continue;
      }

      *nameend = '\0';		/* we can do this!  End here!! I mean it */

      /* Now match the realm of the mail address with the list of
       * valid mail realms.
       */

      if (!string_in_list(realmp, mail_realm_list(auth_dat->prealm), 1)) {
	syslog(LOG_ERR, "Mail realm doesn't match valid list, \"%s\".",
	       realmp);
	*error_msg = "Mail address in user ID does not match list of valid addresses";
	continue;
      }

      /* Now check the userID that we have for invalid characters */
      if (strpbrk(userp, disallowed_chars) != NULL) {
	syslog(LOG_ERR, "Invalid characters in username: %s", userp);
	*error_msg = "Invalid characters in username";
	continue;
      }      

      /* If I've gotten here, then userp points to a string that is
       * a useful PGP userID.  Save this, and the return 0.
       */
      *username = (char *)malloc(strlen(userp)+1);
      strcpy(*username, userp);
      *error_msg = NULL;
      return 0;
      break;

    default:
      syslog(LOG_DEBUG, "Key not found.");
      *error_msg = "Unlikely error occurred in server";
      return -1;

    } /* switch */

  } /* while */

  *error_msg = "End-of-file obtained without reading any key information";
  return -1;
}

/*
 * Save the PGP key to a temporary keyfile, verify the name
 * on the key, and if it matches, then sign the key, and then
 * extract the key again, and remove the keyfile.
 */
int
verify_and_sign_key(char *key, int keylen, AUTH_DAT *auth_dat, 
		    char **signedkey, int *signedkeylen, char **error_msg)
{
  FILE *pgp;
  char keyring[BUFSIZ];
  char bakring[BUFSIZ];
  char keyout[BUFSIZ];
  char *username, *tempkey;
  int tempkeylen, retval;

  /* Check Args */
  assert (key != NULL);
  assert (auth_dat != NULL);
  assert (signedkey != NULL);
  assert (signedkeylen != NULL);
  assert (error_msg != NULL);

  *error_msg = NULL;

  /* Generate a tempfile name */
  sprintf(keyring, "%s/pubring_%d.pgp", PGPPATH, getpid());
  sprintf(bakring, "%s/pubring_%d.bak", PGPPATH, getpid());
  sprintf(keyout, "%s/pubring_%d.out", PGPPATH, getpid());

  if ((retval = add_key_to_keyring(key, keylen, keyring, keyout, error_msg))
      != 0) {
    free(key);
    return(retval);
  }
  free(key);

  pgp = fopen(keyout, "r");

  /* Compare response with kerberos authorization */
  if (verify_key(pgp, auth_dat, &username, error_msg) != 0) {
    syslog(LOG_ERR, "Key verification failed: %s", *error_msg);
    pclose(pgp);
    unlink(keyring);
    unlink(bakring);
    unlink(keyout);
    return -1;
  }
  fclose(pgp);
  unlink(keyout);

  syslog(LOG_INFO, "Verfication succeeded, signing key for <%s>.", username);

  /* First, I need to put my public key onto the new keyring.  Extract
   * this key from my keyring, and then add it to the temp keyring.
   */
  if ((retval = extract_key_from_keyring("*", "", &tempkey, &tempkeylen)) 
      != 0) {
    unlink(keyring);
    unlink(bakring);
    *error_msg = "Error adding server key to keyring:  popen failed";
    return retval;
  }

  if ((retval = add_key_to_keyring(tempkey, tempkeylen, keyring, "/dev/null",
				   error_msg)) != 0) {
    free(tempkey);
    unlink(keyring);
    unlink(bakring);
    *error_msg = "Error adding server key to keyring:  popen failed";
    return retval;
  }
  free(tempkey);

  /* Try to sign key, need to send passphrase to PGP program */
  if ((retval = sign_key(username, keyring, error_msg)) != 0) {
    unlink(keyring);
    unlink(bakring);
    return retval;
  }

  syslog(LOG_DEBUG, "Extracting key for user.");

  /* Try to extract the key, now */
  if ((retval = extract_key_from_keyring(username, keyring, signedkey, 
					 signedkeylen)) != 0) {
    unlink(keyring);
    unlink(bakring);
    *error_msg = "Error trying to load signed key:  popen failed";
    return -1;
  }

  unlink(keyring);
  unlink(bakring);

  return(0);
}
