/* pfs-signatures.c: 
 *
 ****************************************************************
 * Copyright (C) 2003 Tom Lord
 * 
 * See the file "COPYING" for further information about
 * the copyright and warranty status of this work.
 */


#include "config-include/config-options.h"
#include "hackerlab/bugs/panic.h"
#include "hackerlab/os/sys/types.h"
#include "hackerlab/os/sys/wait.h"
#include "hackerlab/os/signal.h"
#include "hackerlab/char/str.h"
#include "hackerlab/hash/md5-utils.h"
#include "hackerlab/rx-posix/regex.h"
#include "hackerlab/vu/safe.h"
#include "hackerlab/fs/file-names.h"
#include "tla/libfsutils/file-contents.h"
#include "tla/libfsutils/tmp-files.h"
#include "tla/libawk/associative.h"
#include "tla/libawk/trim.h"
#include "tla/libarch/my.h"
#include "tla/libarch/namespace.h"
#include "tla/libarch/archives.h"
#include "tla/libarch/pfs-signatures.h"


/* __STDC__ prototypes for static functions */
static void exec_shell_command (t_uchar * command);
static t_uchar * archive_signing_rule_file (t_uchar * archive, int no_default);
static t_uchar * archive_signature_checking_rule_file (t_uchar * archive, int no_default);
static int arch_pfs_memoize_checksum_file (t_uchar * archive, t_uchar * official_archive, t_uchar * revision, t_uchar * file_contents);



int
arch_pfs_has_signing_rule (t_uchar * archive)
{
  t_uchar * rule_file = 0;
  int answer = 0;

  rule_file = archive_signing_rule_file (archive, 1);
  answer = !!rule_file;

  lim_free (0, rule_file);
  return answer;
}



int
arch_pfs_sign_for_archive (t_uchar * archive, t_uchar * official_archive, t_uchar * revision, t_uchar * sigfile, int in_fd, int out_fd)
{
  t_uchar * rule_file = 0;
  t_uchar * rule = 0;
  int pid;
  int answer = 0;

  rule_file = archive_signing_rule_file (archive, 0);

  if (!rule_file)
    {
      safe_printfmt (2, ("\n"
                         "\n"
                         "********************************\n"
                         "SIGNATURE DEMANDED FOR ARCHIVE\n"
                         "  %s\n"
                         "BUT NO RULE PROVIDED\n"
                         "\n"
                         "Consider creating ~/.arch-params/signing/%s\n"
                         " or ~/.arch-params/signing/=default\n"
                         "\n"
                         "********************************\n"
                         "\n"
                         "\n"),
                     archive, archive);

    generic_error_exit:

      safe_printfmt (2, ("\n"
                         "(You may also have to use tla lock-revision -b before\n"
                         " retrying this transaction.  See tla lock-revision -H)\n"
                         "\n"));
      answer = -1;

    generic_return:

      lim_free (0, rule_file);
      lim_free (0, rule);

      return answer;
    }

  rule = file_contents (rule_file);
  rule = trim_surrounding_ws (rule);

  if (arch_valid_archive_name (rule))
    {
      /* Copy signature from another archive. */
      struct arch_archive * sig_from = 0;
      struct arch_pfs_archive * pfs_sig_from;
      t_uchar * revdir = 0;
      t_uchar * sigpath = 0;
      t_uchar * signed_checksum = 0;

      sig_from = arch_archive_connect (rule, 0);

      revdir = arch_fs_archive_revision_path (0, revision);
      sigpath = file_name_in_vicinity (0, revdir, sigfile);

      if (str_cmp (sig_from->vtable->type, "pfs"))
        {
          safe_printfmt (2, ("\n"
                             "\n"
                             "********************************\n"
                             "\n"
                             "ARCHIVE SIGNING RULE SAYS TO COPY\n"
                             "CHECKSUMS FROM %s\n"
                             "\n"
                             "SURPRISINGLY, ARCH DOESN'T KNOW\n"
                             "HOW TO COPY FROM THAT ARCHIVE.\n"
                             "\n"
                             "  your rule file: %s\n"
                             "\n"
                             "********************************\n"
                             "\n"
                             "\n"),
                         rule, rule_file);
          exit (2);
        }

      pfs_sig_from = (struct arch_pfs_archive *)sig_from;
      signed_checksum = arch_pfs_file_contents (pfs_sig_from->pfs, sigpath, 1);

      if (!signed_checksum || arch_pfs_memoize_checksum_file (archive, official_archive, revision, signed_checksum))
        {
          safe_printfmt (2, ("\n"
                             "\n"
                             "********************************\n"
                             "\n"
                             "ARCHIVE SIGNING RULE SAYS TO COPY\n"
                             "CHECKSUMS FROM %s\n"
                             "\n"
                             "THE CHECKSUM FILE IN THAT ARCHIVE FOR\n"
                             "\n"
                             "  %s\n"
                             "\n"
                             "IS MISSING OR INAPPROPRIATE.\n"
                             "\n"
                             "  your rule file: %s\n"
                             "\n"
                             "********************************\n"
                             "\n"
                             "\n"),
                         rule, revision, rule_file);
          exit (2);
        }

      safe_write_retry (out_fd, signed_checksum, str_length (signed_checksum));

      arch_archive_close (sig_from);
      lim_free (0, signed_checksum);
      lim_free (0, revdir);
      lim_free (0, sigpath);
      return 0;
    }
  else
    {
      /* Generate the signature anew */

      pid = fork ();

      if (pid == -1)
        {
          safe_printfmt (2, "unable to fork to run %s\n", rule);
          goto generic_error_exit;
        }

      if (pid)
        {
          int status;
          int wait_pid;

          wait_pid = waitpid (pid, &status, 0);
          if (wait_pid < 0)
            {
              panic_msg ("error waiting for signature generation subprocess");
              kill (0, SIGKILL);
              panic ("error waiting for subprocess");
            }
          if (WIFSIGNALED (status))
            {
              safe_printfmt (2, "\n");
              safe_printfmt (2, "signature subprocess killed by signal %d\n", WTERMSIG (status));
              safe_printfmt (2, "\n");
              goto generic_error_exit;
            }
          else if (!WIFEXITED (status))
            {
              panic_msg ("waitpid returned for a non-exited process");
              kill (0, SIGKILL);
              panic ("waitpid returned for a non-exited process");
            }
          else
            {
              int exit_status;

              exit_status = WEXITSTATUS (status);

              if (exit_status)
                {
                  safe_printfmt (2, ("signature command exited with non-0 status (%d)\n"
                                     "\n"
                                     "command: %s\n"
                                     "\n"),
                                 exit_status,
                                 rule);

                  goto generic_error_exit;
                }
              else
                goto generic_return;
            }
        }
      else
        {
          safe_move_fd (in_fd, 0);
          safe_move_fd (out_fd, 1);

          exec_shell_command (rule);

          panic ("not reached");
        }

      panic ("not reached");
      kill (0, SIGKILL);
      return -1;
    }
}


int
arch_pfs_check_signature_for_archive (t_uchar * archive, t_uchar * signed_message)
{
  t_uchar * rule_file = 0;
  t_uchar * rule = 0;
  t_uchar * tmp_path = 0;
  int tmp_fd = -1;
  int pid;
  int answer = 0;

  rule_file = archive_signature_checking_rule_file (archive, 0);

  if (!rule_file)
    {
    generic_return:

      lim_free (0, rule_file);
      lim_free (0, rule);
      lim_free (0, tmp_path);
      if (tmp_fd > 0)
        safe_close (tmp_fd);

      return answer;
    }

  tmp_path = tmp_file_name ("/tmp", "checksum-contents");
  tmp_fd = safe_open (tmp_path, O_RDWR | O_CREAT | O_EXCL, 0);
  safe_unlink (tmp_path);
  safe_write_retry (tmp_fd, signed_message, str_length (signed_message));
  safe_lseek (tmp_fd, (off_t)0, SEEK_SET);

  rule = file_contents (rule_file);

  pid = fork ();

  if (pid == -1)
    {
      safe_printfmt (2, "unable to fork to run %s\n", rule);
      answer = -1;
      goto generic_return;
    }

  if (pid)
    {
      int status;
      int wait_pid;

      wait_pid = waitpid (pid, &status, 0);
      if (wait_pid < 0)
        {
          panic_msg ("error waiting for signature checking subprocess");
          kill (0, SIGKILL);
          panic ("error waiting for subprocess");
        }
      if (WIFSIGNALED (status))
        {
          safe_printfmt (2, "\n");
          safe_printfmt (2, "signature checking subprocess killed by signal %d\n", WTERMSIG (status));
          safe_printfmt (2, "\n");
          answer = -1;
          goto generic_return;
        }
      else if (!WIFEXITED (status))
        {
          panic_msg ("waitpid returned for a non-exited process");
          kill (0, SIGKILL);
          panic ("waitpid returned for a non-exited process");
        }
      else
        {
          int exit_status;

          exit_status = WEXITSTATUS (status);

          if (exit_status)
            answer = -1;

          goto generic_return;
        }
    }
  else
    {
      safe_move_fd (tmp_fd, 0);
      
      exec_shell_command (rule);

      panic ("not reached");
    }

  panic ("not reached");
  kill (0, SIGKILL);
  return -1;
}



static void
exec_shell_command (t_uchar * command)
{
  char * argv[5];

  argv[0] = "sh";
  argv[1] = "-e";
  argv[2] = "-c";
  argv[3] = (char *)command;
  argv[4] = 0;

  execv (cfg__posix_shell, argv);

  panic ("not reached");
}


static t_uchar * 
archive_signing_rule_file (t_uchar * archive, int no_default)
{
  t_uchar * home;
  t_uchar * params_dir = 0;
  t_uchar * rules_dir = 0;
  t_uchar * answer = 0;

  home = getenv ("HOME");
  invariant (!!home);

  params_dir = arch_my_arch_params ();
  rules_dir = file_name_in_vicinity (0, params_dir, "signing");
  
  answer = file_name_in_vicinity (0, rules_dir, archive);
  
  if (safe_access (answer, F_OK))
    {
      lim_free (0, answer);
      answer = 0;

      if (!no_default)
        {
          answer = file_name_in_vicinity (0, rules_dir, "=default");

          if (safe_access (answer, F_OK))
            {
              lim_free (0, answer);
              answer = 0;
            }
        }
    }


  lim_free (0, params_dir);
  lim_free (0, rules_dir);

  return answer;
}


static t_uchar *
archive_signature_checking_rule_file (t_uchar * archive, int no_default)
{
  static assoc_table already_checked = 0;
  static assoc_table already_know = 0;

  t_uchar * check_rule_name = 0;
  t_uchar * home;
  t_uchar * params_dir = 0;
  t_uchar * rules_dir = 0;
  t_uchar * answer = 0;

  answer = assoc_ref (already_know, archive);

  if (answer)
    {
      answer = str_save (0, answer);
    }
  else if (!assoc_ref (already_checked, archive))
    {
      check_rule_name = str_alloc_cat (0, archive, ".check");

      home = getenv ("HOME");
      invariant (!!home);
      
      params_dir = arch_my_arch_params ();
      rules_dir = file_name_in_vicinity (0, params_dir, "signing");
      
      answer = file_name_in_vicinity (0, rules_dir, check_rule_name);
  
      if (safe_access (answer, F_OK))
        {
          lim_free (0, answer);

          if (no_default)
            answer = 0;
          else
            {
              answer = file_name_in_vicinity (0, rules_dir, "=default.check");

              if (safe_access (answer, F_OK))
                {
                  lim_free (0, answer);
                  answer = 0;
                }
            }
        }

      if (answer || !no_default)
        {
          assoc_set (&already_checked, archive, "yes");
          if (answer)
            {
              assoc_set (&already_know, archive, answer);
            }
          else
            {
              safe_printfmt (2, ("\n"
                                 "WARNING: no rule found for checking signatures from %s\n"
                                 "\n"
                                 "  Consider creating ~/.arch-params/signing/%s.check\n"
                                 "  or ~/.arch-params/signing/=default.check\n"
                                 "\n"),
                             archive, archive);
            }
        }
    }

  lim_free (0, check_rule_name);
  lim_free (0, params_dir);
  lim_free (0, rules_dir);

  return answer;
}




static assoc_table remembered_md5s = 0;
/*
 * keys are
 * 		$archive $revision $file
 * 
 * values are (ascii, hex) md5 checksums.
 */


static assoc_table checked_revisions = 0;
/*
 * keys are
 * 		$archive $revision
 * 
 * values are "yes" meaning that the checksum
 * file for that revision has been read.
 */


static assoc_table checksummed_revisions = 0;
/*
 * keys are
 * 		$archive $revision
 * 
 * values are "yes" meaning that a checksum
 * file for that revision was found.
 */



int
arch_pfs_ensure_checksum_data (struct arch_pfs_archive * arch, t_uchar * revision)
{
  int answer = 0;
  t_uchar * revision_key = 0;

  revision_key = str_alloc_cat_many (0, arch->arch.name, " ", revision, str_end);

  if (!assoc_ref (checked_revisions, revision_key))
    {
      t_uchar * revision_dir = 0;
#     define N_CHECKSUM_FILES 2
      static char * checksum_file_name[N_CHECKSUM_FILES] = { "checksum", "checksum.cacherev" };
      int x;
      t_uchar * check_rule = 0;
      int found_checksums = 0;

      /* If the user thinks he wants to signature-check this archive,
       * but the archive doesn't think it's signed, then freak out.
       */
      check_rule = archive_signature_checking_rule_file (arch->arch.name, 1);

      if (check_rule && !arch->arch.signed_archive)
        {
          safe_printfmt (2, ("\n"
                             "\n"
                             "********************************\n"
                             "\n"
                             "ARCHIVE NOT SIGNED BUT YOU HAVE A RULE\n"
                             "FOR SIGNATURE CHECKING THIS ARCHIVE.\n"
                             "\n"
                             "  archive: %s\n"
                             "  revision %s\n"
                             "\n"
                             "  your rule file: %s\n"
                             "\n"
                             "********************************\n"
                             "\n"
                             "\n"),
                         arch->arch.name, revision, check_rule);
          answer = -1;
        }

      revision_dir = arch_fs_archive_revision_path (0, revision);

      for (x = 0; x < N_CHECKSUM_FILES; ++x)
        {
          t_uchar * file_path = 0;
          t_uchar * file_contents = 0;

          file_path = file_name_in_vicinity (0, revision_dir, checksum_file_name[x]);
          file_contents = arch_pfs_file_contents (arch->pfs, file_path, 1);

          if (file_contents)
            {
              assoc_set (&checksummed_revisions, revision_key, "yes");
              found_checksums = 1;
            }

          if (arch->arch.signed_archive && file_contents && arch_pfs_check_signature_for_archive (arch->arch.name, file_contents))
            {
              safe_printfmt (2, ("\n"
                                 "\n"
                                 "********************************\n"
                                 "\n"
                                 "INVALID SIGNATURE ON REVISION!\n"
                                 "  archive: %s\n"
                                 "  revision %s\n"
                                 "  checksum file: %s\n"
                                 "\n"
                                 "********************************\n"
                                 "\n"
                                 "\n"),
                             arch->arch.name, revision, checksum_file_name[x]);
              answer = -1;
            }
          else if (file_contents && arch_pfs_memoize_checksum_file (arch->arch.name, arch->arch.official_name, revision, file_contents))
            {
              safe_printfmt (2, ("\n"
                                 "\n"
                                 "********************************\n"
                                 "\n"
                                 "INVALID CHECKSUM FILE SYNTAX FOR REVISION!\n"
                                 "  archive: %s\n"
                                 "  revision %s\n"
                                 "  checksum file: %s\n"
                                 "\n"
                                 "********************************\n"
                                 "\n"
                                 "\n"),
                             arch->arch.name, revision, checksum_file_name[x]);
              answer = -1;
            }

          lim_free (0, file_path);
          lim_free (0, file_contents);
        }

      assoc_set (&checked_revisions, revision_key, "yes");

      /* If the archive is supposed to be signed, but no checksums 
       * were found, fail.   If the archive is not signed, but no 
       * checksums were found, warn.
       */

      if (!found_checksums)
        {
          if (arch->arch.signed_archive || check_rule)
            {
              safe_printfmt (2, ("\n"
                                 "\n"
                                 "********************************\n"
                                 "\n"
                                 "NO CHECKSUMS FOUND FOR REVISION!\n"
                                 " IN ARCHIVE THAT'S SUPPOSED TO\n"
                                 " BE SIGNED!\n"
                                 "\n"
                                 "  archive: %s\n"
                                 "  revision %s\n"
                                 "\n"
                                 "********************************\n"
                                 "\n"
                                 "\n"),
                             arch->arch.name, revision);
              answer = -1;
            }
          else
            {
              safe_printfmt (2, ("\n"
                                 "********************************\n"
                                 "NO CHECKSUMS FOUND FOR REVISION\n"
                                 " (unsigned archive, continuing anyway)\n"
                                 "\n"
                                 "  archive: %s\n"
                                 "  revision %s\n"
                                 "\n"
                                 "********************************\n"
                                 "\n"),
                             arch->arch.name, revision);
            }
        }
    
      lim_free (0, revision_dir);
      lim_free (0, check_rule);
    }

  lim_free (0, revision_key);

  return answer;
}


static int
arch_pfs_memoize_checksum_file (t_uchar * archive, t_uchar * official_archive, t_uchar * revision, t_uchar * file_contents)
{
  static char file_pattern[] = ("^Signature-for: (.*)/(.*)\n"
                                "([[:(md5 [^/[:space:]]+ ................................\n):]]*)");
  static int is_compiled = 0;
  static regex_t preg;

  regmatch_t pmatch[4];


  if (!is_compiled)
    {
      if (regcomp (&preg, file_pattern, REG_EXTENDED | REG_NEWLINE))
        panic ("unable to compile checksum-file regexp");
      is_compiled = 1;
    }

  if (regexec (&preg, file_contents, 4, pmatch, 0))
    return -1;

  if (str_cmp_n (official_archive, str_length (official_archive),
                 file_contents + pmatch[1].rm_so, (pmatch[1].rm_eo - pmatch[1].rm_so)))
    return -1;

  if (str_cmp_n (revision, str_length (revision),
                 file_contents + pmatch[2].rm_so, (pmatch[2].rm_eo - pmatch[2].rm_so)))
    return -1;


  {
    t_uchar * item_start;
    t_uchar * boundary;
    t_uchar * key_prefix = 0;

    item_start = file_contents + pmatch[3].rm_so;
    boundary = file_contents + pmatch[3].rm_eo;

    key_prefix = str_alloc_cat_many (0, archive, " ", revision, " ", str_end);

    while (item_start < boundary)
      {
        t_uchar * item_end;
        t_uchar * file_name = 0;
        t_uchar * assoc_key = 0;
        t_uchar * md5 = 0;

        item_start = 1 + str_chr_index (item_start, ' ');
        item_end = str_chr_index (item_start, ' ');

        file_name = str_save_n (0, item_start, item_end - item_start);
        assoc_key = str_alloc_cat (0, key_prefix, file_name);
        md5 = str_save_n (0, item_end + 1, 32);

        item_start = item_end + 34; /* space 32-byte-md5-string newline */

        assoc_set (&remembered_md5s, assoc_key, md5);

        lim_free (0, file_name);
        lim_free (0, assoc_key);
        lim_free (0, md5);
      }

    lim_free (0, key_prefix);
  }

  return 0;
}

int
arch_pfs_checksum_anticipates_file (t_uchar * archive, t_uchar * revision, t_uchar * file)
{
  t_uchar * key = 0;
  int answer;

  key = str_alloc_cat_many (0, archive, " ", revision, " ", file, str_end);

  answer = !!assoc_ref (remembered_md5s, key);

  lim_free (0, key);
  return answer;
}


int
arch_pfs_checksum_governs (t_uchar * archive, t_uchar * revision)
{
  t_uchar * key = 0;
  int answer;

#undef FIXME
  /* Under what circumstance should it be an error that a revision
   * lacks a checksum file?   (*_ensure_* abover should handle this)
   */

  key = str_alloc_cat_many (0, archive, " ", revision, str_end);

  answer = !!assoc_ref (checksummed_revisions, key);

  lim_free (0, key);
  return answer;
}


int
arch_pfs_checksum_governs_strictly (struct arch_pfs_archive * arch)
{
  t_uchar * check_rule = 0;
  int answer = 0;

  if (arch->arch.signed_archive)
    return 1;
  else
    {
      check_rule = archive_signature_checking_rule_file (arch->arch.name, 1);
      if (check_rule)
        answer = 1;
    }

  lim_free (0, check_rule);
  return answer;
}


t_uchar *
arch_pfs_checked_file_contents (struct arch_pfs_archive * arch, t_uchar * revision, t_uchar * path)
{
  if (!arch_pfs_checksum_governs (arch->arch.name, revision))
    return arch_pfs_file_contents (arch->pfs, path, 0);
  else
    {
      t_uchar * tail = 0;
      t_uchar * key = 0;
      t_uchar * remembered_md5;
      t_uchar * contents = 0;
      t_uchar * contents_md5 = 0;

      tail = file_name_tail (0, path);
      key = str_alloc_cat_many (0, arch->arch.name, " ", revision, " ", tail, str_end);
      remembered_md5 = assoc_ref (remembered_md5s, key);

      contents = arch_pfs_file_contents (arch->pfs, path, 0);
      contents_md5 = md5_ascii_for_str (0, contents);

      if (remembered_md5 && str_cmp (remembered_md5, contents_md5))
        {
          safe_printfmt (2, ("\n"
                             "********************************\n"
                             " MISMATCHED ARCHIVE CHECKSUM\n"
                             "\n"
                             "  archive: %s\n"
                             "  revision: %s\n"
                             "  file: %s\n"
                             "\n"
                             "  expected: %s\n"
                             "  got: %s\n"
                             "\n"
                             "********************************\n"
                             "\n"),
                         arch->arch.name, revision, tail, remembered_md5, contents_md5);
          exit (2);
        }

      lim_free (0, tail);
      lim_free (0, key);
      lim_free (0, contents_md5);

      return contents;
    }
}


void
arch_pfs_checked_get_file (struct arch_pfs_archive * arch, t_uchar * revision, int out_fd, t_uchar * path)
{
  if (!arch_pfs_checksum_governs (arch->arch.name, revision))
    arch_pfs_get_file (arch->pfs, out_fd, path, 0);
  else
    {
      t_uchar * tail = 0;
      t_uchar * key = 0;
      t_uchar * remembered_md5;
      t_uchar * tmp_path = 0;
      int tmp_fd = -1;
      t_uchar buf[1 << 16];
      md5_context_t md5c = 0;
      t_uchar computed_md5_raw[16];
      t_uchar * computed_md5 = 0;
      
      tail = file_name_tail (0, path);
      key = str_alloc_cat_many (0, arch->arch.name, " ", revision, " ", tail, str_end);
      remembered_md5 = assoc_ref (remembered_md5s, key);

      tmp_path = tmp_file_name ("/tmp", "arch-file");
      tmp_fd = safe_open (tmp_path, O_RDWR | O_CREAT | O_EXCL, 0);
      safe_unlink (tmp_path);
      arch_pfs_get_file (arch->pfs, tmp_fd, path, 0);
      safe_lseek (tmp_fd, (off_t)0, SEEK_SET);

      md5c = make_md5_context (0);

      while (1)
        {
          ssize_t amt;

          amt = safe_read_retry (tmp_fd, buf, sizeof (buf));

          if (!amt)
            break;
          else
            {
              md5_scan (md5c, buf, (size_t)amt);
              safe_write_retry (out_fd, buf, (size_t)amt);
            }
        }

      md5_final (computed_md5_raw, md5c);
      computed_md5 = md5_alloc_ascii (0, computed_md5_raw);

      if (remembered_md5 && str_cmp (remembered_md5, computed_md5))
        {
          safe_printfmt (2, ("\n"
                             "********************************\n"
                             " MISMATCHED ARCHIVE CHECKSUM\n"
                             "\n"
                             "  archive: %s\n"
                             "  revision: %s\n"
                             "  file: %s\n"
                             "\n"
                             "  expected: %s\n"
                             "  got: %s\n"
                             "\n"
                             "********************************\n"
                             "\n"),
                         arch->arch.name, revision, tail, remembered_md5, computed_md5);
          exit (2);
        }

      lim_free (0, tail);
      lim_free (0, key);
      lim_free (0, tmp_path);
      safe_close (tmp_fd);
      free_md5_context (0, md5c);
      lim_free (0, computed_md5);
    }
}



/* tag: Tom Lord Wed Dec 24 19:41:23 2003 (pfs-signatures.c)
 */
