/* t-ed25519.c - Check the Ed25519 crypto
 * Copyright (C) 2013 g10 Code GmbH
 *
 * This file is part of Libgcrypt.
 *
 * Libgcrypt is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * Libgcrypt 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <stdarg.h>
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#include "../src/gcrypt-int.h"

#include "stopwatch.h"

#define PGM "t-ed25519"
#define N_TESTS 1026

#define my_isascii(c) (!((c) & 0x80))
#define digitp(p)   (*(p) >= '0' && *(p) <= '9')
#define hexdigitp(a) (digitp (a)                     \
                      || (*(a) >= 'A' && *(a) <= 'F')  \
                      || (*(a) >= 'a' && *(a) <= 'f'))
#define xtoi_1(p)   (*(p) <= '9'? (*(p)- '0'): \
                     *(p) <= 'F'? (*(p)-'A'+10):(*(p)-'a'+10))
#define xtoi_2(p)   ((xtoi_1(p) * 16) + xtoi_1((p)+1))
#define xmalloc(a)    gcry_xmalloc ((a))
#define xcalloc(a,b)  gcry_xcalloc ((a),(b))
#define xstrdup(a)    gcry_xstrdup ((a))
#define xfree(a)      gcry_free ((a))
#define pass()        do { ; } while (0)

static int verbose;
static int debug;
static int error_count;
static int sign_with_pk;
static int no_verify;
static int custom_data_file;

static void
die (const char *format, ...)
{
  va_list arg_ptr ;

  fflush (stdout);
  fprintf (stderr, "%s: ", PGM);
  va_start( arg_ptr, format ) ;
  vfprintf (stderr, format, arg_ptr );
  va_end(arg_ptr);
  if (*format && format[strlen(format)-1] != '\n')
    putc ('\n', stderr);
  exit (1);
}

static void
fail (const char *format, ...)
{
  va_list arg_ptr;

  fflush (stdout);
  fprintf (stderr, "%s: ", PGM);
  /* if (wherestr) */
  /*   fprintf (stderr, "%s: ", wherestr); */
  va_start (arg_ptr, format);
  vfprintf (stderr, format, arg_ptr);
  va_end (arg_ptr);
  if (*format && format[strlen(format)-1] != '\n')
    putc ('\n', stderr);
  error_count++;
  if (error_count >= 50)
    die ("stopped after 50 errors.");
}

static void
show (const char *format, ...)
{
  va_list arg_ptr;

  if (!verbose)
    return;
  fprintf (stderr, "%s: ", PGM);
  va_start (arg_ptr, format);
  vfprintf (stderr, format, arg_ptr);
  if (*format && format[strlen(format)-1] != '\n')
    putc ('\n', stderr);
  va_end (arg_ptr);
}


static void
show_note (const char *format, ...)
{
  va_list arg_ptr;

  if (!verbose && getenv ("srcdir"))
    fputs ("      ", stderr);  /* To align above "PASS: ".  */
  else
    fprintf (stderr, "%s: ", PGM);
  va_start (arg_ptr, format);
  vfprintf (stderr, format, arg_ptr);
  if (*format && format[strlen(format)-1] != '\n')
    putc ('\n', stderr);
  va_end (arg_ptr);
}


static void
show_sexp (const char *prefix, gcry_sexp_t a)
{
  char *buf;
  size_t size;

  fprintf (stderr, "%s: ", PGM);
  if (prefix)
    fputs (prefix, stderr);
  size = gcry_sexp_sprint (a, GCRYSEXP_FMT_ADVANCED, NULL, 0);
  buf = xmalloc (size);

  gcry_sexp_sprint (a, GCRYSEXP_FMT_ADVANCED, buf, size);
  fprintf (stderr, "%.*s", (int)size, buf);
  gcry_free (buf);
}


/* Prepend FNAME with the srcdir environment variable's value and
   retrun an allocated filename. */
char *
prepend_srcdir (const char *fname)
{
  static const char *srcdir;
  char *result;

  if (!srcdir && !(srcdir = getenv ("srcdir")))
    srcdir = ".";

  result = xmalloc (strlen (srcdir) + 1 + strlen (fname) + 1);
  strcpy (result, srcdir);
  strcat (result, "/");
  strcat (result, fname);
  return result;
}


/* Read next line but skip over empty and comment lines.  Caller must
   xfree the result.  */
static char *
read_textline (FILE *fp, int *lineno)
{
  char line[4096];
  char *p;

  do
    {
      if (!fgets (line, sizeof line, fp))
        {
          if (feof (fp))
            return NULL;
          die ("error reading input line: %s\n", strerror (errno));
        }
      ++*lineno;
      p = strchr (line, '\n');
      if (!p)
        die ("input line %d not terminated or too long\n", *lineno);
      *p = 0;
      for (p--;p > line && my_isascii (*p) && isspace (*p); p--)
        *p = 0;
    }
  while (!*line || *line == '#');
  /* if (debug) */
  /*   show ("read line: '%s'\n", line); */
  return xstrdup (line);
}


/* Copy the data after the tag to BUFFER.  BUFFER will be allocated as
   needed.  */
static void
copy_data (char **buffer, const char *line, int lineno)
{
  const char *s;

  xfree (*buffer);
  *buffer = NULL;

  s = strchr (line, ':');
  if (!s)
    {
      fail ("syntax error at input line %d", lineno);
      return;
    }
  for (s++; my_isascii (*s) && isspace (*s); s++)
    ;
  *buffer = xstrdup (s);
}


/* Convert STRING consisting of hex characters into its binary
   representation and return it as an allocated buffer. The valid
   length of the buffer is returned at R_LENGTH.  The string is
   delimited by end of string.  The function returns NULL on
   error.  */
static void *
hex2buffer (const char *string, size_t *r_length)
{
  const char *s;
  unsigned char *buffer;
  size_t length;

  buffer = xmalloc (strlen(string)/2+1);
  length = 0;
  for (s=string; *s; s +=2 )
    {
      if (!hexdigitp (s) || !hexdigitp (s+1))
        return NULL;           /* Invalid hex digits. */
      ((unsigned char*)buffer)[length++] = xtoi_2 (s);
    }
  *r_length = length;
  return buffer;
}


static void
hexdowncase (char *string)
{
  char *p;

  for (p=string; *p; p++)
    if (my_isascii (*p))
      *p = tolower (*p);
}


static void
one_test (int testno, const char *sk, const char *pk,
          const char *msg, const char *sig)
{
  gpg_error_t err;
  int i;
  char *p;
  void *buffer = NULL;
  void *buffer2 = NULL;
  size_t buflen, buflen2;
  gcry_sexp_t s_tmp, s_tmp2;
  gcry_sexp_t s_sk = NULL;
  gcry_sexp_t s_pk = NULL;
  gcry_sexp_t s_msg= NULL;
  gcry_sexp_t s_sig= NULL;
  unsigned char *sig_r = NULL;
  unsigned char *sig_s = NULL;
  char *sig_rs_string = NULL;
  size_t sig_r_len, sig_s_len;

  if (verbose > 1)
    show ("Running test %d\n", testno);

  if (!(buffer = hex2buffer (sk, &buflen)))
    {
      fail ("error building s-exp for test %d, %s: %s",
            testno, "sk", "invalid hex string");
      goto leave;
    }
  if (!(buffer2 = hex2buffer (pk, &buflen2)))
    {
      fail ("error building s-exp for test %d, %s: %s",
            testno, "pk", "invalid hex string");
      goto leave;
    }
  if (sign_with_pk)
    err = gcry_sexp_build (&s_sk, NULL,
                           "(private-key"
                           " (ecc"
                           "  (curve \"Ed25519\")"
                           "  (flags eddsa)"
                           "  (q %b)"
                           "  (d %b)))",
                           (int)buflen2, buffer2,
                           (int)buflen, buffer);
  else
    err = gcry_sexp_build (&s_sk, NULL,
                           "(private-key"
                           " (ecc"
                           "  (curve \"Ed25519\")"
                           "  (flags eddsa)"
                           "  (d %b)))",
                           (int)buflen, buffer);
  if (err)
    {
      fail ("error building s-exp for test %d, %s: %s",
            testno, "sk", gpg_strerror (err));
      goto leave;
    }

  if ((err = gcry_sexp_build (&s_pk, NULL,
                              "(public-key"
                              " (ecc"
                              "  (curve \"Ed25519\")"
                              "  (flags eddsa)"
                              "  (q %b)))",  (int)buflen2, buffer2)))
    {
      fail ("error building s-exp for test %d, %s: %s",
            testno, "pk", gpg_strerror (err));
      goto leave;
    }

  xfree (buffer);
  if (!(buffer = hex2buffer (msg, &buflen)))
    {
      fail ("error building s-exp for test %d, %s: %s",
            testno, "msg", "invalid hex string");
      goto leave;
    }
  if ((err = gcry_sexp_build (&s_msg, NULL,
                              "(data"
                              " (flags eddsa)"
                              " (hash-algo sha512)"
                              " (value %b))",  (int)buflen, buffer)))
    {
      fail ("error building s-exp for test %d, %s: %s",
            testno, "msg", gpg_strerror (err));
      goto leave;
    }

  if ((err = gcry_pk_sign (&s_sig, s_msg, s_sk)))
    fail ("gcry_pk_sign failed for test %d: %s", testno, gpg_strerror (err));
  if (debug)
    show_sexp ("sig=", s_sig);

  s_tmp2 = NULL;
  s_tmp = gcry_sexp_find_token (s_sig, "sig-val", 0);
  if (s_tmp)
    {
      s_tmp2 = s_tmp;
      s_tmp = gcry_sexp_find_token (s_tmp2, "eddsa", 0);
      if (s_tmp)
        {
          gcry_sexp_release (s_tmp2);
          s_tmp2 = s_tmp;
          s_tmp = gcry_sexp_find_token (s_tmp2, "r", 0);
          if (s_tmp)
            {
              sig_r = gcry_sexp_nth_buffer (s_tmp, 1, &sig_r_len);
              gcry_sexp_release (s_tmp);
            }
          s_tmp = gcry_sexp_find_token (s_tmp2, "s", 0);
          if (s_tmp)
            {
              sig_s = gcry_sexp_nth_buffer (s_tmp, 1, &sig_s_len);
              gcry_sexp_release (s_tmp);
            }
        }
    }
  gcry_sexp_release (s_tmp2); s_tmp2 = NULL;

  if (!sig_r || !sig_s)
    fail ("gcry_pk_sign failed for test %d: %s", testno, "r or s missing");
  else
    {
      sig_rs_string = xmalloc (2*(sig_r_len + sig_s_len)+1);
      p = sig_rs_string;
      *p = 0;
      for (i=0; i < sig_r_len; i++, p += 2)
        snprintf (p, 3, "%02x", sig_r[i]);
      for (i=0; i < sig_s_len; i++, p += 2)
        snprintf (p, 3, "%02x", sig_s[i]);
      if (strcmp (sig_rs_string, sig))
        {
          fail ("gcry_pk_sign failed for test %d: %s",
                testno, "wrong value returned");
          show ("  expected: '%s'", sig);
          show ("       got: '%s'", sig_rs_string);
        }
    }

  if (!no_verify)
    if ((err = gcry_pk_verify (s_sig, s_msg, s_pk)))
      fail ("gcry_pk_verify failed for test %d: %s",
            testno, gpg_strerror (err));


 leave:
  gcry_sexp_release (s_sig);
  gcry_sexp_release (s_sk);
  gcry_sexp_release (s_pk);
  gcry_sexp_release (s_msg);
  xfree (buffer);
  xfree (buffer2);
  xfree (sig_r);
  xfree (sig_s);
  xfree (sig_rs_string);
}


static void
check_ed25519 (const char *fname)
{
  FILE *fp;
  int lineno, ntests;
  char *line;
  int testno;
  char *sk, *pk, *msg, *sig;

  show ("Checking Ed25519.\n");

  fp = fopen (fname, "r");
  if (!fp)
    die ("error opening '%s': %s\n", fname, strerror (errno));

  testno = 0;
  sk = pk = msg = sig = NULL;
  lineno = ntests = 0;
  while ((line = read_textline (fp, &lineno)))
    {
      if (!strncmp (line, "TST:", 4))
        testno = atoi (line+4);
      else if (!strncmp (line, "SK:", 3))
        copy_data (&sk, line, lineno);
      else if (!strncmp (line, "PK:", 3))
        copy_data (&pk, line, lineno);
      else if (!strncmp (line, "MSG:", 4))
        copy_data (&msg, line, lineno);
      else if (!strncmp (line, "SIG:", 4))
        copy_data (&sig, line, lineno);
      else
        fail ("unknown tag at input line %d", lineno);

      xfree (line);
      if (testno && sk && pk && msg && sig)
        {
          hexdowncase (sig);
          one_test (testno, sk, pk, msg, sig);
          ntests++;
          if (!(ntests % 256))
            show_note ("%d of %d tests done\n", ntests, N_TESTS);
          xfree (pk);  pk = NULL;
          xfree (sk);  sk = NULL;
          xfree (msg); msg = NULL;
          xfree (sig); sig = NULL;
        }

    }
  xfree (pk);
  xfree (sk);
  xfree (msg);
  xfree (sig);

  if (ntests != N_TESTS && !custom_data_file)
    fail ("did %d tests but expected %d", ntests, N_TESTS);
  else if ((ntests % 256))
    show_note ("%d tests done\n", ntests);

  fclose (fp);
}


int
main (int argc, char **argv)
{
  int last_argc = -1;
  char *fname = NULL;

  if (argc)
    { argc--; argv++; }

  while (argc && last_argc != argc )
    {
      last_argc = argc;
      if (!strcmp (*argv, "--"))
        {
          argc--; argv++;
          break;
        }
      else if (!strcmp (*argv, "--help"))
        {
          fputs ("usage: " PGM " [options]\n"
                 "Options:\n"
                 "  --verbose       print timings etc.\n"
                 "  --debug         flyswatter\n"
                 "  --sign-with-pk  also use the public key for signing\n"
                 "  --no-verify     skip the verify test\n"
                 "  --data FNAME    take test data from file FNAME\n",
                 stdout);
          exit (0);
        }
      else if (!strcmp (*argv, "--verbose"))
        {
          verbose++;
          argc--; argv++;
        }
      else if (!strcmp (*argv, "--debug"))
        {
          verbose += 2;
          debug++;
          argc--; argv++;
        }
      else if (!strcmp (*argv, "--sign-with-pk"))
        {
          sign_with_pk = 1;
          argc--; argv++;
        }
      else if (!strcmp (*argv, "--no-verify"))
        {
          no_verify = 1;
          argc--; argv++;
        }
      else if (!strcmp (*argv, "--data"))
        {
          argc--; argv++;
          if (argc)
            {
              xfree (fname);
              fname = xstrdup (*argv);
              argc--; argv++;
            }
        }
      else if (!strncmp (*argv, "--", 2))
        die ("unknown option '%s'", *argv);

    }

  if (!fname)
    fname = prepend_srcdir ("t-ed25519.inp");
  else
    custom_data_file = 1;

  gcry_control (GCRYCTL_DISABLE_SECMEM, 0);
  if (!gcry_check_version (GCRYPT_VERSION))
    die ("version mismatch\n");
  if (debug)
    gcry_control (GCRYCTL_SET_DEBUG_FLAGS, 1u , 0);
  gcry_control (GCRYCTL_ENABLE_QUICK_RANDOM, 0);
  gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0);

  start_timer ();
  check_ed25519 (fname);
  stop_timer ();

  xfree (fname);

  show ("All tests completed in %s.  Errors: %d\n",
        elapsed_time (), error_count);
  return !!error_count;
}
