/*
  This file is part of TALER
  Copyright (C) 2026 Taler Systems SA

  TALER 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, or (at your option) any later version.

  TALER 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
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file util/crypto_ecdsa.c
 * @brief ECDSA over NIST P-256
 * @author Bohdan Potuzhnyi
 * @author Volodymyr Potuzhnyi
 *
 * GNUnet's ECDSA is over Ed25519, so P-256 is implemented here on top
 * of libgcrypt. This module knows nothing about what is being
 * signed: callers pass a hash and receive raw fixed-width scalars.
 */
#include "platform.h"
#include "taler/taler_util.h"
#include <gcrypt.h>


/**
 * Curve name libgcrypt uses.
 */
#define P256_CURVE "NIST P-256"

/**
 * Size of a scalar: the private key, and each of r and s.
 */
#define P256_SCALAR_LEN 32

/**
 * Order of the group, used to canonicalize signatures to low-s.
 */
#define P256_ORDER_HEX \
  "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551"


/**
 * Write @a v into @a buf, zero-padded on the left to exactly @a len
 * bytes. Needed because libgcrypt returns MPIs without leading
 * zeros, while the wire format uses fixed-width scalars.
 *
 * @param v value to write
 * @param[out] buf where to write the value
 * @param len number of bytes to write
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
mpi_to_fixed (gcry_mpi_t v,
              unsigned char *buf,
              size_t len)
{
  size_t nbytes;

  if (0 != gcry_mpi_print (GCRYMPI_FMT_USG,
                           NULL,
                           0,
                           &nbytes,
                           v))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  if (nbytes > len)
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  memset (buf,
          0,
          len);
  if (0 != gcry_mpi_print (GCRYMPI_FMT_USG,
                           &buf[len - nbytes],
                           nbytes,
                           &nbytes,
                           v))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


/**
 * Extract the MPI @a token from the s-expression @a sexp.
 *
 * @param sexp s-expression to search
 * @param token name of the token to extract
 * @return the value, or NULL on error; caller must release
 */
static gcry_mpi_t
sexp_extract_mpi (gcry_sexp_t sexp,
                  const char *token)
{
  gcry_sexp_t t;
  gcry_mpi_t ret;

  t = gcry_sexp_find_token (sexp,
                            token,
                            0);
  if (NULL == t)
  {
    GNUNET_break (0);
    return NULL;
  }
  ret = gcry_sexp_nth_mpi (t,
                           1,
                           GCRYMPI_FMT_USG);
  gcry_sexp_release (t);
  if (NULL == ret)
    GNUNET_break (0);
  return ret;
}


/**
 * Is @a s in the upper half of the group order? ECDSA accepts both
 * @a s and n-s; only the low variant is canonical.
 *
 * @param s scalar to check
 * @param[out] high set to true if @a s is the high variant
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
s_is_high (gcry_mpi_t s,
           bool *high)
{
  gcry_mpi_t n;
  gcry_mpi_t half;

  if (0 != gcry_mpi_scan (&n,
                          GCRYMPI_FMT_HEX,
                          P256_ORDER_HEX,
                          0,
                          NULL))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  half = gcry_mpi_new (256);
  gcry_mpi_rshift (half,
                   n,
                   1);
  *high = (gcry_mpi_cmp (s,
                         half) > 0);
  gcry_mpi_release (half);
  gcry_mpi_release (n);
  return GNUNET_OK;
}


/**
 * Replace @a s by n-s, canonicalizing a signature to low-s.
 *
 * @param[in,out] s scalar to negate modulo the group order
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
s_to_low (gcry_mpi_t s)
{
  gcry_mpi_t n;

  if (0 != gcry_mpi_scan (&n,
                          GCRYMPI_FMT_HEX,
                          P256_ORDER_HEX,
                          0,
                          NULL))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  gcry_mpi_sub (s,
                n,
                s);
  gcry_mpi_release (n);
  return GNUNET_OK;
}


enum GNUNET_GenericReturnValue
TALER_ecdsa_p256_key_create (
  struct TALER_EcdsaP256PrivateKeyP *priv,
  struct TALER_EcdsaP256PublicKeyP *pub)
{
  gcry_sexp_t params = NULL;
  gcry_sexp_t keypair = NULL;
  gcry_mpi_t d = NULL;
  gcry_mpi_t q = NULL;
  unsigned char qbuf[2 * P256_SCALAR_LEN + 1];
  enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;

  if (0 != gcry_sexp_build (&params,
                            NULL,
                            "(genkey(ecc(curve \"" P256_CURVE "\")))"))
  {
    GNUNET_break (0);
    goto cleanup;
  }
  if (0 != gcry_pk_genkey (&keypair,
                           params))
  {
    GNUNET_break (0);
    goto cleanup;
  }
  d = sexp_extract_mpi (keypair,
                        "d");
  q = sexp_extract_mpi (keypair,
                        "q");
  if ( (NULL == d) ||
       (NULL == q) )
    goto cleanup;
  if (GNUNET_OK !=
      mpi_to_fixed (d,
                    priv->d,
                    sizeof (priv->d)))
    goto cleanup;
  /* libgcrypt hands us the uncompressed point 0x04|X|Y; we store the
     compressed form, whose prefix encodes the parity of Y */
  if (GNUNET_OK !=
      mpi_to_fixed (q,
                    qbuf,
                    sizeof (qbuf)))
    goto cleanup;
  if (0x04 != qbuf[0])
  {
    GNUNET_break (0);
    goto cleanup;
  }
  pub->q[0] = (qbuf[sizeof (qbuf) - 1] & 1)
              ? 0x03
              : 0x02;
  memcpy (&pub->q[1],
          &qbuf[1],
          P256_SCALAR_LEN);
  ret = GNUNET_OK;
cleanup:
  if (NULL != d)
    gcry_mpi_release (d);
  if (NULL != q)
    gcry_mpi_release (q);
  if (NULL != keypair)
    gcry_sexp_release (keypair);
  if (NULL != params)
    gcry_sexp_release (params);
  return ret;
}


enum GNUNET_GenericReturnValue
TALER_ecdsa_p256_sign (
  const struct TALER_EcdsaP256PrivateKeyP *priv,
  const struct GNUNET_ShortHashCode *hash,
  struct TALER_EcdsaP256SignatureP *sig)
{
  gcry_sexp_t skey = NULL;
  gcry_sexp_t data = NULL;
  gcry_sexp_t sigs = NULL;
  gcry_mpi_t hm = NULL;
  gcry_mpi_t r = NULL;
  gcry_mpi_t s = NULL;
  enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
  bool high;

  if (0 != gcry_sexp_build (&skey,
                            NULL,
                            "(private-key(ecc(curve \"" P256_CURVE "\")"
                            "(d %b)))",
                            (int) sizeof (priv->d),
                            (const char *) priv->d))
  {
    GNUNET_break (0);
    goto cleanup;
  }
  if (0 != gcry_mpi_scan (&hm,
                          GCRYMPI_FMT_USG,
                          hash,
                          sizeof (*hash),
                          NULL))
  {
    GNUNET_break (0);
    goto cleanup;
  }
  if (0 != gcry_sexp_build (&data,
                            NULL,
                            "(data(flags raw)(value %m))",
                            hm))
  {
    GNUNET_break (0);
    goto cleanup;
  }
  if (0 != gcry_pk_sign (&sigs,
                         data,
                         skey))
  {
    GNUNET_break (0);
    goto cleanup;
  }
  r = sexp_extract_mpi (sigs,
                        "r");
  s = sexp_extract_mpi (sigs,
                        "s");
  if ( (NULL == r) ||
       (NULL == s) )
    goto cleanup;
  if (GNUNET_OK !=
      s_is_high (s,
                 &high))
    goto cleanup;
  if (high &&
      (GNUNET_OK !=
       s_to_low (s)) )
    goto cleanup;
  if ( (GNUNET_OK !=
        mpi_to_fixed (r,
                      sig->r_s,
                      P256_SCALAR_LEN)) ||
       (GNUNET_OK !=
        mpi_to_fixed (s,
                      &sig->r_s[P256_SCALAR_LEN],
                      P256_SCALAR_LEN)) )
    goto cleanup;
  ret = GNUNET_OK;
cleanup:
  if (NULL != r)
    gcry_mpi_release (r);
  if (NULL != s)
    gcry_mpi_release (s);
  if (NULL != hm)
    gcry_mpi_release (hm);
  if (NULL != sigs)
    gcry_sexp_release (sigs);
  if (NULL != data)
    gcry_sexp_release (data);
  if (NULL != skey)
    gcry_sexp_release (skey);
  return ret;
}


enum GNUNET_GenericReturnValue
TALER_ecdsa_p256_verify (
  const struct TALER_EcdsaP256PublicKeyP *pub,
  const struct GNUNET_ShortHashCode *hash,
  const struct TALER_EcdsaP256SignatureP *sig)
{
  gcry_sexp_t pkey = NULL;
  gcry_sexp_t data = NULL;
  gcry_sexp_t sigs = NULL;
  gcry_mpi_t hm = NULL;
  gcry_mpi_t r = NULL;
  gcry_mpi_t s = NULL;
  enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
  bool high;

  if (0 != gcry_mpi_scan (&r,
                          GCRYMPI_FMT_USG,
                          sig->r_s,
                          P256_SCALAR_LEN,
                          NULL))
  {
    GNUNET_break_op (0);
    goto cleanup;
  }
  if (0 != gcry_mpi_scan (&s,
                          GCRYMPI_FMT_USG,
                          &sig->r_s[P256_SCALAR_LEN],
                          P256_SCALAR_LEN,
                          NULL))
  {
    GNUNET_break_op (0);
    goto cleanup;
  }
  /* only the canonical low-s form is accepted, so that a signature
     cannot be mauled into a second valid encoding */
  if (GNUNET_OK !=
      s_is_high (s,
                 &high))
    goto cleanup;
  if (high)
  {
    GNUNET_break_op (0);
    goto cleanup;
  }
  if (0 != gcry_mpi_scan (&hm,
                          GCRYMPI_FMT_USG,
                          hash,
                          sizeof (*hash),
                          NULL))
  {
    GNUNET_break (0);
    goto cleanup;
  }
  if (0 != gcry_sexp_build (&data,
                            NULL,
                            "(data(flags raw)(value %m))",
                            hm))
  {
    GNUNET_break (0);
    goto cleanup;
  }
  /* libgcrypt decompresses the point and rejects anything that is not
     on the curve, so an invalid public key fails here */
  if (0 != gcry_sexp_build (&pkey,
                            NULL,
                            "(public-key(ecc(curve \"" P256_CURVE "\")"
                            "(q %b)))",
                            (int) sizeof (pub->q),
                            (const char *) pub->q))
  {
    GNUNET_break_op (0);
    goto cleanup;
  }
  if (0 != gcry_sexp_build (&sigs,
                            NULL,
                            "(sig-val(ecdsa(r %m)(s %m)))",
                            r,
                            s))
  {
    GNUNET_break (0);
    goto cleanup;
  }
  ret = (0 == gcry_pk_verify (sigs,
                              data,
                              pkey))
        ? GNUNET_OK
        : GNUNET_SYSERR;
cleanup:
  if (NULL != r)
    gcry_mpi_release (r);
  if (NULL != s)
    gcry_mpi_release (s);
  if (NULL != hm)
    gcry_mpi_release (hm);
  if (NULL != sigs)
    gcry_sexp_release (sigs);
  if (NULL != data)
    gcry_sexp_release (data);
  if (NULL != pkey)
    gcry_sexp_release (pkey);
  return ret;
}


/* end of crypto_ecdsa.c */
