/* $Cambridge: exim/exim-src/src/dk.c,v 1.11 2006/10/30 22:06:33 tom Exp $ */

/*************************************************
*     Exim - an Internet mail transport agent    *
*************************************************/

/* Copyright (c) University of Cambridge 1995 - 2006 */
/* See the file NOTICE for conditions of use and distribution. */

/* Code for DomainKeys support. Other DK relevant code is in
   receive.c, transport.c and transports/smtp.c */

#include "exim.h"

#ifdef EXPERIMENTAL_DOMAINKEYS

/* Globals related to the DK reference library. */
DK                   *dk_context             = NULL;
DK_LIB               *dk_lib                 = NULL;
DK_FLAGS              dk_flags;
DK_STAT               dk_internal_status;

/* Globals related to Exim DK implementation. */
dk_exim_verify_block *dk_verify_block        = NULL;

/* Global char buffer for getc/ungetc functions. We need
   to accumulate some chars to be able to match EOD and
   doubled SMTP dots. Those must not be fed to the validation
   engine. */
int dkbuff[6] = {256,256,256,256,256,256};

/* receive_getc() wrapper that feeds DK while Exim reads
   the message. */
int dk_receive_getc(void) {
  int i;
  int c = receive_getc();

  if (dk_context != NULL) {
    /* Send oldest byte */
    if ((dkbuff[0] < 256) && (dk_internal_status == DK_STAT_OK)) {
      dk_internal_status = dk_message(dk_context, CUS &dkbuff[0], 1);
      if (dk_internal_status != DK_STAT_OK)
        DEBUG(D_receive) debug_printf("DK: %s\n", DK_STAT_to_string(dk_internal_status));
    }
    /* rotate buffer */
    for (i=0;i<5;i++) dkbuff[i]=dkbuff[i+1];
    dkbuff[5]=c;
    /* look for our candidate patterns */
    if ( (dkbuff[1] == '\r') &&
         (dkbuff[2] == '\n') &&
         (dkbuff[3] == '.') &&
         (dkbuff[4] == '\r') &&
         (dkbuff[5] == '\n') ) {
      /* End of DATA */
      dkbuff[3] = 256;
      dkbuff[4] = 256;
      dkbuff[5] = 256;
    }
    if ( (dkbuff[2] == '\r') &&
         (dkbuff[3] == '\n') &&
         (dkbuff[4] == '.') &&
         (dkbuff[5] == '.') ) {
      /* doubled dot, skip this char */
      dkbuff[5] = 256;
    }
  }
return c;
}

/* When exim puts a char back in the fd, we
   must rotate our buffer back. */
int dk_receive_ungetc(int c) {
  int i;
  if (dk_context != NULL) {
    /* rotate buffer back */
    for (i=5;i>0;i--) dkbuff[i]=dkbuff[i-1];
    dkbuff[0]=256;
  }
  return receive_ungetc(c);
}


void dk_exim_verify_init(void) {
  int old_pool = store_pool;
  store_pool = POOL_PERM;

  /* Reset DK state in any case. */
  dk_context = NULL;
  dk_lib = NULL;
  dk_verify_block = NULL;

  /* Set up DK context if DK was requested and input is SMTP. */
  if (smtp_input && !smtp_batched_input && dk_do_verify) {
    /* initialize library */
    dk_lib = dk_init(&dk_internal_status);
    if (dk_internal_status != DK_STAT_OK)
      debug_printf("DK: %s\n", DK_STAT_to_string(dk_internal_status));
    else {
      /* initialize verification context */
      dk_context = dk_verify(dk_lib, &dk_internal_status);
      if (dk_internal_status != DK_STAT_OK) {
        debug_printf("DK: %s\n", DK_STAT_to_string(dk_internal_status));
        dk_context = NULL;
      }
      else {
        /* Reserve some space for the verify block. */
        dk_verify_block = store_get(sizeof(dk_exim_verify_block));
        if (dk_verify_block == NULL) {
          debug_printf("DK: Can't allocate %d bytes.\n",sizeof(dk_exim_verify_block));
          dk_context = NULL;
        }
        else {
          memset(dk_verify_block, 0, sizeof(dk_exim_verify_block));
        }
      }
    }
  }
  store_pool = old_pool;
}


void dk_exim_verify_finish(void) {
  char *p,*q;
  int i;
  int old_pool = store_pool;

  /* Bail out if context could not be set up earlier. */
  if (dk_context == NULL)
    return;

  store_pool = POOL_PERM;

  /* Send remaining bytes from input which are still in the buffer. */
  for (i=0;i<6;i++)
    if (dkbuff[i] < 256)
      dk_internal_status = dk_message(dk_context, CUS &dkbuff[i], 1);

  /* Flag end-of-message. */
  dk_internal_status = dk_end(dk_context, &dk_flags);

  /* dk_flags now has the selector flags (if there was one).
     It seems that currently only the "t=" flag is supported
     in selectors. */
  if (dk_flags & DK_FLAG_SET)
    if (dk_flags & DK_FLAG_TESTING)
      dk_verify_block->testing = TRUE;

  /* Grab address/domain information. */
  p = dk_address(dk_context);
  if (p != NULL) {
    switch(p[0]) {
      case 'N':
        dk_verify_block->address_source = DK_EXIM_ADDRESS_NONE;
      break;
      case 'S':
        dk_verify_block->address_source = DK_EXIM_ADDRESS_FROM_SENDER;
      break;
      case 'F':
        dk_verify_block->address_source = DK_EXIM_ADDRESS_FROM_FROM;
      break;
    }
    p++;
    if (*p != '\0') {
      dk_verify_block->address = string_copy((uschar *)p);
      q = strrchr(p,'@');
      if ((q != NULL) && (*(q+1) != '\0')) {
        dk_verify_block->domain = string_copy((uschar *)(q+1));
        *q = '\0';
        dk_verify_block->local_part = string_copy((uschar *)p);
        *q = '@';
      }
    }
  }

  /* Now grab the domain-wide DK policy */
  dk_flags = dk_policy(dk_context);

  if (dk_flags & DK_FLAG_SET) {
    /* Selector "t=" flag has precedence, don't overwrite it if
       the selector has set it above. */
    if ((dk_flags & DK_FLAG_TESTING) && !dk_verify_block->testing)
      dk_verify_block->testing = TRUE;
    if (dk_flags & DK_FLAG_SIGNSALL)
      dk_verify_block->signsall = TRUE;
  }

  /* Set up main result. */
  switch(dk_internal_status)
    {
    case DK_STAT_NOSIG:
      dk_verify_block->is_signed = FALSE;
      dk_verify_block->result = DK_EXIM_RESULT_NO_SIGNATURE;
    break;
    case DK_STAT_OK:
      dk_verify_block->is_signed = TRUE;
      dk_verify_block->result = DK_EXIM_RESULT_GOOD;
    break;
    case DK_STAT_BADSIG:
      dk_verify_block->is_signed = TRUE;
      dk_verify_block->result = DK_EXIM_RESULT_BAD;
    break;
    case DK_STAT_REVOKED:
      dk_verify_block->is_signed = TRUE;
      dk_verify_block->result = DK_EXIM_RESULT_REVOKED;
    break;
    case DK_STAT_BADKEY:
    case DK_STAT_SYNTAX:
      dk_verify_block->is_signed = TRUE;
      /* Syntax -> Bad format? */
      dk_verify_block->result = DK_EXIM_RESULT_BAD_FORMAT;
    break;
    case DK_STAT_NOKEY:
      dk_verify_block->is_signed = TRUE;
      dk_verify_block->result = DK_EXIM_RESULT_NO_KEY;
    break;
    case DK_STAT_NORESOURCE:
    case DK_STAT_INTERNAL:
    case DK_STAT_ARGS:
    case DK_STAT_CANTVRFY:
      dk_verify_block->result = DK_EXIM_RESULT_ERR;
    break;
    /* This is missing DK_EXIM_RESULT_NON_PARTICIPANT. The lib does not
       report such a status. */
    }

  /* Set up human readable result string. */
  dk_verify_block->result_string = string_copy((uschar *)DK_STAT_to_string(dk_internal_status));

  /* All done, reset dk_context. */
  dk_free(dk_context,1);
  dk_context = NULL;

  store_pool = old_pool;
}

uschar *dk_exim_sign(int dk_fd,
                     uschar *dk_private_key,
                     uschar *dk_domain,
                     uschar *dk_selector,
                     uschar *dk_canon) {
  uschar *rc = NULL;
  uschar *headers = NULL;
  int headers_len;
  int dk_canon_int = DK_CANON_SIMPLE;
  char buf[4096];
  int seen_lf = 0;
  int seen_lfdot = 0;
  uschar sig[1024];
  int save_errno = 0;
  int sread;
  int old_pool = store_pool;
  store_pool = POOL_PERM;

  dk_lib = dk_init(&dk_internal_status);
  if (dk_internal_status != DK_STAT_OK) {
    debug_printf("DK: %s\n", DK_STAT_to_string(dk_internal_status));
    rc = NULL;
    goto CLEANUP;
  }

  /* Figure out what canonicalization to use. Unfortunately
     we must do this BEFORE knowing which domain we sign for. */
  if ((dk_canon != NULL) && (Ustrcmp(dk_canon, "nofws") == 0)) dk_canon_int = DK_CANON_NOFWS;
  else dk_canon = US "simple";

  /* Initialize signing context. */
  dk_context = dk_sign(dk_lib, &dk_internal_status, dk_canon_int);
  if (dk_internal_status != DK_STAT_OK) {
    debug_printf("DK: %s\n", DK_STAT_to_string(dk_internal_status));
    dk_context = NULL;
    goto CLEANUP;
  }

  while((sread = read(dk_fd,&buf,4096)) > 0) {
    int pos = 0;
    char c;

    while (pos < sread) {
      c = buf[pos++];

      if ((c == '.') && seen_lfdot) {
        /* escaped dot, write "\n.", continue */
        dk_message(dk_context, CUS "\n.", 2);
        seen_lf = 0;
        seen_lfdot = 0;
        continue;
      }

      if (seen_lfdot) {
        /* EOM, write "\n" and break */
        dk_message(dk_context, CUS "\n", 1);
        break;
      }

      if ((c == '.') && seen_lf) {
        seen_lfdot = 1;
        continue;
      }

      if (seen_lf) {
        /* normal lf, just send it */
        dk_message(dk_context, CUS "\n", 1);
        seen_lf = 0;
      }

      if (c == '\n') {
        seen_lf = 1;
        continue;
      }

      /* write the char */
      dk_message(dk_context, CUS &c, 1);
    }
  }

  /* Handle failed read above. */
  if (sread == -1) {
    debug_printf("DK: Error reading -K file.\n");
    save_errno = errno;
    rc = NULL;
    goto CLEANUP;
  }

  /* Flag end-of-message. */
  dk_internal_status = dk_end(dk_context, NULL);
  /* TODO: check status */


  /* Get domain to use, unless overridden. */
  if (dk_domain == NULL) {
    dk_domain = US dk_address(dk_context);
    switch(dk_domain[0]) {
      case 'N': dk_domain = NULL; break;
      case 'F':
      case 'S':
        dk_domain++;
        dk_domain = Ustrrchr(dk_domain,'@');
        if (dk_domain != NULL) {
          uschar *p;
          dk_domain++;
          p = dk_domain;
          while (*p != 0) { *p = tolower(*p); p++; }
        }
      break;
    }
    if (dk_domain == NULL) {
      debug_printf("DK: Could not determine domain to use for signing from message headers.\n");
      /* In this case, we return "OK" by sending up an empty string as the
         DomainKey-Signature header. If there is no domain to sign for, we
         can send the message anyway since the recipient has no policy to
         apply ... */
      rc = US"";
      goto CLEANUP;
    }
  }
  else {
    dk_domain = expand_string(dk_domain);
    if (dk_domain == NULL) {
      /* expansion error, do not send message. */
      debug_printf("DK: Error while expanding dk_domain option.\n");
      rc = NULL;
      goto CLEANUP;
    }
  }

  /* Set up $dk_domain expansion variable. */
  dk_signing_domain = dk_domain;

  /* Get selector to use. */
  dk_selector = expand_string(dk_selector);
  if (dk_selector == NULL) {
    log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand "
      "dk_selector: %s", expand_string_message);
    rc = NULL;
    goto CLEANUP;
  }

  /* Set up $dk_selector expansion variable. */
  dk_signing_selector = dk_selector;

  /* Get private key to use. */
  dk_private_key = expand_string(dk_private_key);
  if (dk_private_key == NULL) {
    log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand "
      "dk_private_key: %s", expand_string_message);
    rc = NULL;
    goto CLEANUP;
  }

  if ( (Ustrlen(dk_private_key) == 0) ||
       (Ustrcmp(dk_private_key,"0") == 0) ||
       (Ustrcmp(dk_private_key,"false") == 0) ) {
    /* don't sign, but no error */
    rc = US"";
    goto CLEANUP;
  }

  if (dk_private_key[0] == '/') {
    int privkey_fd = 0;
    /* Looks like a filename, load the private key. */
    memset(big_buffer,0,big_buffer_size);
    privkey_fd = open(CS dk_private_key,O_RDONLY);
    (void)read(privkey_fd,big_buffer,16383);
    (void)close(privkey_fd);
    dk_private_key = big_buffer;
  }

  /* Get the signature. */
  dk_internal_status = dk_getsig(dk_context, dk_private_key, sig, 1024);

  /* Check for unuseable key */
  if (dk_internal_status != DK_STAT_OK) {
    debug_printf("DK: %s\n", DK_STAT_to_string(dk_internal_status));
    rc = NULL;
    goto CLEANUP;
  }

  headers_len = dk_headers(dk_context, NULL);
  rc = store_get(1024+256+headers_len);
  headers = store_malloc(headers_len);
  dk_headers(dk_context, CS headers);
  /* Build DomainKey-Signature header to return. */
  (void)string_format(rc, 1024+256+headers_len, "DomainKey-Signature: a=rsa-sha1; q=dns; c=%s; s=%s; d=%s;\r\n"
                     "\th=%s;\r\n"
                     "\tb=%s;\r\n", dk_canon, dk_selector, dk_domain, headers, sig);

  log_write(0, LOG_MAIN, "DK: message signed using a=rsa-sha1; q=dns; c=%s; s=%s; d=%s; h=%s;", dk_canon, dk_selector, dk_domain, headers);
  store_free(headers);

  CLEANUP:
  if (dk_context != NULL) {
    dk_free(dk_context,1);
    dk_context = NULL;
  }
  store_pool = old_pool;
  errno = save_errno;
  return rc;
}

#endif
