/*
 This file is part of TALER
 Copyright (C) 2023-2024 Taler Systems SA

 TALER is free software; you can redistribute it and/or modify it under the
 terms of the GNU Affero 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 Affero General Public License for more details.

 You should have received a copy of the GNU Affero General Public License along with
 TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */
/**
 * @file donau-httpd_keys.c
 * @brief management of our various keys
 * @author Christian Grothoff
 * @author Özgür Kesim
 * @author Pius Loosli
 * @author Johannes Casaburi
 */
#include <donau_config.h>
#include <taler/taler_json_lib.h>
#include <taler/taler_mhd_lib.h>
#include "donau_json_lib.h"
#include "donau-httpd.h"
#include "donau-httpd_keys.h"
#include "donau-httpd_config.h"
#include "donaudb_plugin.h"
#include "donau_util.h"


/**
 * @brief All information about an donau online signing key (which is used to
 * sign messages from the donau).
 */
struct SigningKey
{

  /**
   * The donau's (online signing) public key.
   */
  struct DONAU_DonauPublicKeyP donau_pub;

  /**
   * Meta data about the signing key, such as validity periods.
   */
  struct DONAUDB_SignkeyMetaData meta;

  /**
   * Did we lose the private keys? // NEEDED?
   */
  bool lost;
};


/**
 * Snapshot of the (coin and signing) keys (including private keys) of
 * the exchange.  There can be multiple instances of this struct, as it is
 * reference counted and only destroyed once the last user is done
 * with it.  The current instance is acquired using
 * #TEH_KS_acquire().  Using this function increases the
 * reference count.  The contents of this structure (except for the
 * reference counter) should be considered READ-ONLY until it is
 * ultimately destroyed (as there can be many concurrent users).
 */
struct DH_KeyStateHandle
{

  /**
   * For which (global) key_generation was this data structure created?
   * Used to check when we are outdated and need to be re-generated.
   */
  uint64_t key_generation;

  /**
   * When did we initiate the key reloading?
   */
  struct GNUNET_TIME_Timestamp reload_time;

  /**
   * When does our online signing key expire and we
   * thus need to re-generate this response?
   */
  struct GNUNET_TIME_Timestamp signature_expires;

  /**
   * Response to return if the client supports (deflate) compression.
   */
  struct MHD_Response *response_compressed;

  /**
   * Response to return if the client does not support compression.
   */
  struct MHD_Response *response_uncompressed;

  /**
   * ETag for these responses.
   */
  char *etag;

};


/**
 * RSA security module public key, all zero if not known.
 */
static struct TALER_SecurityModulePublicKeyP donation_unit_rsa_sm_pub;

/**
 * CS security module public key, all zero if not known.
 */
static struct TALER_SecurityModulePublicKeyP donation_unit_cs_sm_pub;

/**
 * EdDSA security module public key, all zero if not known.
 */
static struct TALER_SecurityModulePublicKeyP esign_sm_pub;

/**
 * Counter incremented whenever we have a reason to re-build the keys because
 * something external changed.  See #DH_keys_get_state() and
 * #DH_keys_update_states() for uses of this variable.
 */
static uint64_t key_generation;

/**
 * Handle for the esign/EdDSA helper.
 */
static struct TALER_CRYPTO_ExchangeSignHelper *esh;

/**
 * Handle for the donation_unit/RSA helper.
 */
static struct TALER_CRYPTO_RsaDenominationHelper *rsadh;

/**
 * Handle for the donation_unit/CS helper.
 */
static struct TALER_CRYPTO_CsDenominationHelper *csdh;

/**
 * Map from H(rsa_pub) or H(cs_pub) to `struct DH_DonationUnitKey` entries.
 */
static struct GNUNET_CONTAINER_MultiHashMap *du_keys;

/**
 * Map from `struct TALER_ExchangePublicKey` to `struct SigningKey`
 * entries.  Based on the fact that a `struct GNUNET_PeerIdentity` is also
 * an EdDSA public key.
 */
static struct GNUNET_CONTAINER_MultiPeerMap *esign_keys;

/**
 * Stores the latest generation of our key state.
 */
static struct DH_KeyStateHandle *key_state;


/**
 * Add the headers we want to set for every /keys response.
 *
 * @param cls the key state to use
 * @param[in,out] response the response to modify
 */
static void
setup_general_response_headers (void *cls,
                                struct MHD_Response *response)
{
  struct DH_KeyStateHandle *ksh = cls;
  char dat[128];

  TALER_MHD_add_global_headers (response,
                                true);
  GNUNET_break (
    MHD_YES == MHD_add_response_header (response,
                                        MHD_HTTP_HEADER_CONTENT_TYPE,
                                        "application/json"));
  TALER_MHD_get_date_string (ksh->reload_time.abs_time, dat);
  GNUNET_break (
    MHD_YES == MHD_add_response_header (response,
                                        MHD_HTTP_HEADER_LAST_MODIFIED,
                                        dat));
  /* Set cache control headers: our response varies depending on these headers */
  GNUNET_break (
    MHD_YES == MHD_add_response_header (response,
                                        MHD_HTTP_HEADER_VARY,
                                        MHD_HTTP_HEADER_ACCEPT_ENCODING));
  /* Information is always public, revalidate after 1 hour */
  GNUNET_break (
    MHD_YES == MHD_add_response_header (response,
                                        MHD_HTTP_HEADER_CACHE_CONTROL,
                                        "public,max-age=3600"));
}


/**
 * Initialize @a ksh using the given values for @a signkeys,
 * and @a denoms.
 *
 * @param[in,out] ksh key state handle we build @a ksh for
 * @param[in,out] signkeys list of sign keys to return
 * @param[in,out] donation_units list of grouped denominations to return
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
create_keys_response (struct DH_KeyStateHandle *ksh,
                      json_t *signkeys,
                      json_t *donation_units)
{
  json_t *keys;
  char *keys_json;
  void *keys_jsonz;
  size_t keys_jsonz_size;
  int comp;
  char etag[sizeof (struct GNUNET_HashCode) * 2];

  GNUNET_assert (NULL != signkeys);
  GNUNET_assert (NULL != donation_units);
  GNUNET_assert (NULL != DH_currency);
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Creating /keys response\n");

  keys = GNUNET_JSON_PACK (
    GNUNET_JSON_pack_string ("version",
                             DONAU_PROTOCOL_VERSION),
    GNUNET_JSON_pack_string ("base_url",
                             DH_base_url),
    GNUNET_JSON_pack_string ("currency",
                             DH_currency),
    GNUNET_JSON_pack_array_incref ("signkeys",
                                   signkeys),
    GNUNET_JSON_pack_array_incref ("donation_units",
                                   donation_units));
  GNUNET_assert (NULL != keys);

  /* Convert /keys response to UTF8-String */
  keys_json = json_dumps (keys,
                          JSON_INDENT (2));
  json_decref (keys);
  GNUNET_assert (NULL != keys_json);

  /* Keep copy for later compression... */
  keys_jsonz = GNUNET_strdup (keys_json);
  keys_jsonz_size = strlen (keys_json);

  /* hash to compute etag */
  {
    struct GNUNET_HashCode ehash;
    char *end;

    GNUNET_CRYPTO_hash (keys_jsonz,
                        keys_jsonz_size,
                        &ehash);
    end = GNUNET_STRINGS_data_to_string (&ehash,
                                         sizeof (ehash),
                                         etag,
                                         sizeof (etag));
    *end = '\0';
  }

  /* Create uncompressed response */
  ksh->response_uncompressed
    = MHD_create_response_from_buffer (keys_jsonz_size,
                                       keys_json,
                                       MHD_RESPMEM_MUST_FREE);
  GNUNET_assert (NULL != ksh->response_uncompressed);
  setup_general_response_headers (ksh,
                                  ksh->response_uncompressed);
  GNUNET_break (MHD_YES ==
                MHD_add_response_header (ksh->response_uncompressed,
                                         MHD_HTTP_HEADER_ETAG,
                                         etag));
  /* Also compute compressed version of /keys response */
  comp = TALER_MHD_body_compress (&keys_jsonz,
                                  &keys_jsonz_size);
  ksh->response_compressed
    = MHD_create_response_from_buffer (keys_jsonz_size,
                                       keys_jsonz,
                                       MHD_RESPMEM_MUST_FREE);
  GNUNET_assert (NULL != ksh->response_compressed);
  /* If the response is actually compressed, set the
     respective header. */
  GNUNET_assert ( (MHD_YES != comp) ||
                  (MHD_YES ==
                   MHD_add_response_header (ksh->response_compressed,
                                            MHD_HTTP_HEADER_CONTENT_ENCODING,
                                            "deflate")) );
  setup_general_response_headers (ksh,
                                  ksh->response_compressed);
  /* Set cache control headers: our response varies depending on these headers */
  GNUNET_break (MHD_YES ==
                MHD_add_response_header (ksh->response_compressed,
                                         MHD_HTTP_HEADER_VARY,
                                         MHD_HTTP_HEADER_ACCEPT_ENCODING));
  /* Information is always public, revalidate after 1 day */
  GNUNET_break (MHD_YES ==
                MHD_add_response_header (ksh->response_compressed,
                                         MHD_HTTP_HEADER_CACHE_CONTROL,
                                         "public,max-age=86400"));
  GNUNET_break (MHD_YES ==
                MHD_add_response_header (ksh->response_compressed,
                                         MHD_HTTP_HEADER_ETAG,
                                         etag));
  ksh->etag = GNUNET_strdup (etag);
  return GNUNET_OK;
}


/**
 * Closure for #insert_donation_unit_cb and #add_signkey_cb.
 */
struct KeysBuilderContext
{

  /**
   * Array of donation unit keys.
   */
  json_t *donation_units;

  /**
   * Array of signing keys.
   */
  json_t *signkeys;

};

/**
 * Function called for all signing keys, used to build up the
 * respective JSON response.
 *
 * @param cls a `struct KeysBuilderContext *` with the array to append keys to
 * @param pid the donau public key (in type disguise)
 * @param value a `struct SigningKey`
 * @return #GNUNET_OK (continue to iterate)
 */
static enum GNUNET_GenericReturnValue
add_sign_key_cb (void *cls,
                 const struct GNUNET_PeerIdentity *pid,
                 void *value)
{
  struct KeysBuilderContext *ctx = cls;
  struct SigningKey *sk = value;

  (void) pid;
  GNUNET_assert (
    0 ==
    json_array_append_new (
      ctx->signkeys,
      GNUNET_JSON_PACK (
        GNUNET_JSON_pack_timestamp ("stamp_start",
                                    sk->meta.valid_from),
        GNUNET_JSON_pack_timestamp ("stamp_expire",
                                    sk->meta.expire_sign),
        GNUNET_JSON_pack_data_auto ("key",
                                    &sk->donau_pub))));
  return GNUNET_OK;
}


/**
 * Function called on all of our current and future donation unit keys
 * known to the helper process. Filters out those that are current
 * and adds the remaining donation unit keys (with their configuration
 * data) to the JSON array.
 *
 * @param cls the `struct KeysBuilderContext *`
 * @param h_du_pub hash of the donation unit public key
 * @param value a `struct DH_DonationUnitKey`
 * @return #GNUNET_OK (continue to iterate)
 */
static enum GNUNET_GenericReturnValue
insert_donation_unit_cb (void *cls,
                         const struct GNUNET_HashCode *h_du_pub,
                         void *value)
{
  struct KeysBuilderContext *kbc = cls;
  struct DH_DonationUnitKey *du = value;

  (void) h_du_pub;
  GNUNET_assert (
    0 == json_array_append_new (
      kbc->donation_units,
      GNUNET_JSON_PACK (
        DONAU_JSON_pack_donation_unit_pub ("donation_unit_pub",
                                           &du->donation_unit_pub),
        GNUNET_JSON_pack_uint64 ("year",
                                 du->validity_year),
        GNUNET_JSON_pack_bool ("lost",
                               du->lost),
        TALER_JSON_pack_amount ("value",
                                &du->value)
        )));
  return GNUNET_OK;
}


/**
 * Update the "/keys" responses in @a ksh, computing the detailed replies.
 *
 * This function is to recompute all (including cherry-picked) responses we
 * might want to return, based on the state already in @a ksh.
 *
 * @param[in,out] ksh state handle to update
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
finish_keys_response (struct DH_KeyStateHandle *ksh)
{
  enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
  struct KeysBuilderContext kbc;

  kbc.signkeys = json_array ();
  GNUNET_assert (NULL != kbc.signkeys);
  kbc.donation_units = json_array ();
  GNUNET_assert (NULL != kbc.donation_units);
  GNUNET_CONTAINER_multipeermap_iterate (esign_keys,
                                         &add_sign_key_cb,
                                         &kbc);

  if (0 == json_array_size (kbc.signkeys))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "No online signing keys available. Refusing to generate /keys response.\n");
    ret = GNUNET_NO;
    goto CLEANUP;
  }
  GNUNET_CONTAINER_multihashmap_iterate (du_keys,
                                         &insert_donation_unit_cb,
                                         &kbc);

  if (0 == json_array_size (kbc.donation_units))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "No donation units available. Refusing to generate /keys response.\n");
    ret = GNUNET_NO;
    goto CLEANUP;
  }

  if (GNUNET_OK !=
      create_keys_response (ksh,
                            kbc.signkeys,
                            kbc.donation_units))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Failed to generate key response data\n");
    goto CLEANUP;
  }

  ret = GNUNET_OK;

CLEANUP:
  if (NULL != kbc.donation_units)
    json_decref (kbc.donation_units);
  if (NULL != kbc.signkeys)
    json_decref (kbc.signkeys);
  return ret;
}


/**
 * Free donation unit key data.
 *
 * @param cls a `struct DH_KeyStateHandle`, unused
 * @param h_donation_unit_pub hash of the donation unit public key, unused
 * @param value a `struct DH_DonationUnitKey` to free
 * @return #GNUNET_OK (continue to iterate)
 */
static enum GNUNET_GenericReturnValue
clear_donation_unit_cb (void *cls,
                        const struct GNUNET_HashCode *h_du_pub,
                        void *value)
{
  struct DH_DonationUnitKey *dk = value;

  (void) cls;
  (void) h_du_pub;
  DONAU_donation_unit_pub_free (&dk->donation_unit_pub);
  GNUNET_free (dk);
  return GNUNET_OK;
}


/**
 * Free donation unit key data.
 *
 * @param cls a `struct DH_KeyStateHandle`, unused
 * @param pid the online signing key (type-disguised), unused
 * @param value a `struct SigningKey` to free
 * @return #GNUNET_OK (continue to iterate)
 */
static enum GNUNET_GenericReturnValue
clear_signkey_cb (void *cls,
                  const struct GNUNET_PeerIdentity *pid,
                  void *value)
{
  struct SigningKey *sk = value;

  (void) cls;
  (void) pid;
  GNUNET_free (sk);
  return GNUNET_OK;
}


/**
 * Synchronize helper state. Polls the key helper for updates.
 */
static void
sync_key_helpers (void)
{
  TALER_CRYPTO_helper_rsa_poll (rsadh);
  TALER_CRYPTO_helper_cs_poll (csdh);
  TALER_CRYPTO_helper_esign_poll (esh);
}


/**
 * Check that the given RSA security module's public key is the one
 * we have pinned.  If it does not match, we die hard.
 *
 * @param sm_pub RSA security module public key to check
 */
static void
check_donation_unit_rsa_sm_pub (const struct
                                TALER_SecurityModulePublicKeyP *sm_pub)
{
  if (0 !=
      GNUNET_memcmp (sm_pub,
                     &donation_unit_rsa_sm_pub))
  {
    if (! GNUNET_is_zero (&donation_unit_rsa_sm_pub))
    {
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Our RSA security module changed its key. This must not happen.\n")
      ;
      GNUNET_assert (0);
    }
    donation_unit_rsa_sm_pub = *sm_pub; /* TOFU ;-) */
  }
}


/**
 * Check that the given CS security module's public key is the one
 * we have pinned.  If it does not match, we die hard.
 *
 * @param sm_pub RSA security module public key to check
 */
static void
check_donation_unit_cs_sm_pub (const struct
                               TALER_SecurityModulePublicKeyP *sm_pub)
{
  if (0 !=
      GNUNET_memcmp (sm_pub,
                     &donation_unit_cs_sm_pub))
  {
    if (! GNUNET_is_zero (&donation_unit_cs_sm_pub))
    {
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Our CS security module changed its key. This must not happen.\n")
      ;
      GNUNET_assert (0);
    }
    donation_unit_cs_sm_pub = *sm_pub; /* TOFU ;-) */
  }
}


/**
 * Check that the given EdDSA security module's public key is the one
 * we have pinned.  If it does not match, we die hard.
 *
 * @param sm_pub EdDSA security module public key to check
 */
static void
check_esign_sm_pub (const struct TALER_SecurityModulePublicKeyP *sm_pub)
{
  if (0 !=
      GNUNET_memcmp (sm_pub,
                     &esign_sm_pub))
  {
    if (! GNUNET_is_zero (&esign_sm_pub))
    {
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Our EdDSA security module changed its key. This must not happen.\n")
      ;
      GNUNET_assert (0);
    }
    esign_sm_pub = *sm_pub; /* TOFU ;-) */
  }
}


void
DH_keys_finished ()
{
  if (NULL != rsadh)
  {
    TALER_CRYPTO_helper_rsa_disconnect (rsadh);
    rsadh = NULL;
  }
  if (NULL != csdh)
  {
    TALER_CRYPTO_helper_cs_disconnect (csdh);
    csdh = NULL;
  }
  if (NULL != esh)
  {
    TALER_CRYPTO_helper_esign_disconnect (esh);
    esh = NULL;
  }
  if (NULL != du_keys)
  {
    GNUNET_CONTAINER_multihashmap_iterate (du_keys,
                                           &clear_donation_unit_cb,
                                           NULL);
    GNUNET_CONTAINER_multihashmap_destroy (du_keys);
    du_keys = NULL;
  }
  if (NULL != esign_keys)
  {
    GNUNET_CONTAINER_multipeermap_iterate (esign_keys,
                                           &clear_signkey_cb,
                                           NULL);
    GNUNET_CONTAINER_multipeermap_destroy (esign_keys);
    esign_keys = NULL;
  }
}


static void
destroy_key_state (struct DH_KeyStateHandle *ksh)
{
  if (NULL != ksh->response_compressed)
    MHD_destroy_response (ksh->response_compressed);
  if (NULL != ksh->response_uncompressed)
    MHD_destroy_response (ksh->response_uncompressed);
  GNUNET_free (ksh->etag);
  GNUNET_free (ksh);
}


/**
 * Function called with information about available keys for signing.  Usually
 * only called once per key upon connect. Also called again in case a key is
 * being revoked, in that case with an @a end_time of zero.
 *
 * @param cls NULL
 * @param section_name name of the donation_unit type in the configuration;
 *                 NULL if the key has been revoked or purged
 * @param start_time when does the key become available for signing;
 *                 zero if the key has been revoked or purged
 * @param validity_duration how long does the key remain available for signing;
 *                 zero if the key has been revoked or purged
 * @param h_rsa hash of the @a donation_unit_pub that is available (or was purged)
 * @param bs_pub the public key itself, NULL if the key was revoked or purged
 * @param sm_pub public key of the security module, NULL if the key was revoked or purged
 * @param sm_sig signature from the security module
 */
static void
helper_rsa_cb (
  void *cls,
  const char *section_name,
  struct GNUNET_TIME_Timestamp start_time,
  struct GNUNET_TIME_Relative validity_duration,
  const struct TALER_RsaPubHashP *h_rsa,
  struct GNUNET_CRYPTO_BlindSignPublicKey *bs_pub,
  const struct TALER_SecurityModulePublicKeyP *sm_pub,
  const struct TALER_SecurityModuleSignatureP *sm_sig)
{
  struct DH_DonationUnitKey *du;
  struct TALER_Amount value;
  enum GNUNET_DB_QueryStatus qs;

  (void) cls;
  (void) sm_sig; /* not using offline signatures */
  GNUNET_assert (GNUNET_CRYPTO_BSA_RSA == bs_pub->cipher);
  if (GNUNET_OK !=
      TALER_config_get_amount (DH_cfg,
                               section_name,
                               "value",
                               &value))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "RSA helper provided key for configuration section `%s' that has no `value' option set\n",
                section_name);
    return;
  }
  /* FIXME: could additionally sanity-check that this
     section actually has CIPHER = RSA, etc. */
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "RSA helper announces key %s for donation_unit type %s with validity %s\n",
              GNUNET_h2s (&h_rsa->hash),
              section_name,
              GNUNET_STRINGS_relative_time_to_string (validity_duration,
                                                      false));
  du = GNUNET_CONTAINER_multihashmap_get (du_keys,
                                          &h_rsa->hash);
  if (NULL != du)
  {
    /* only update 'lost' status */
    du->lost = GNUNET_TIME_relative_is_zero (validity_duration);
    return;
  }
  GNUNET_assert (NULL != sm_pub);
  check_donation_unit_rsa_sm_pub (sm_pub);

  du = GNUNET_new (struct DH_DonationUnitKey);
  du->h_donation_unit_pub.hash = h_rsa->hash;
  du->donation_unit_pub.bsign_pub_key
    = GNUNET_CRYPTO_bsign_pub_incref (bs_pub);
  du->validity_year = GNUNET_TIME_time_to_year (start_time.abs_time);
  du->value = value;
  du->lost = GNUNET_TIME_relative_is_zero (validity_duration);

  GNUNET_assert (
    GNUNET_OK ==
    GNUNET_CONTAINER_multihashmap_put (
      du_keys,
      &du->h_donation_unit_pub.hash,
      du,
      GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY));

  qs = DH_plugin->insert_donation_unit (
    DH_plugin->cls,
    &du->h_donation_unit_pub,
    &du->donation_unit_pub,
    du->validity_year,
    &du->value);
  if (qs < 0)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to insert donation units\n");
    GNUNET_SCHEDULER_shutdown ();
    DH_global_ret = EXIT_FAILURE;
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Inserted RSA donation unit of %s\n",
              TALER_amount2s (&value));
  key_generation++;
}


/**
 * Function called with information about available CS keys for signing. Usually
 * only called once per key upon connect. Also called again in case a key is
 * being revoked, in that case with an @a end_time of zero.
 *
 * @param cls NULL
 * @param section_name name of the donation unit type in the configuration;
 *                 NULL if the key has been revoked or purged
 * @param start_time when does the key become available for signing;
 *                 zero if the key has been revoked or purged
 * @param validity_duration how long does the key remain available for signing;
 *                 zero if the key has been revoked or purged
 * @param h_cs hash of the @a donation_unit_pub that is available (or was purged)
 * @param sm_pub the public key itself, NULL if the key was revoked or purged
 * @param sm_pub signature by the security module
 */
static void
helper_cs_cb (
  void *cls,
  const char *section_name,
  struct GNUNET_TIME_Timestamp start_time,
  struct GNUNET_TIME_Relative validity_duration,
  const struct TALER_CsPubHashP *h_cs,
  struct GNUNET_CRYPTO_BlindSignPublicKey *bs_pub,
  const struct TALER_SecurityModulePublicKeyP *sm_pub,
  const struct TALER_SecurityModuleSignatureP *sm_sig)
{
  struct DH_DonationUnitKey *du;
  struct TALER_Amount value;
  enum GNUNET_DB_QueryStatus qs;

  (void) cls;
  (void) sm_sig; /* we are not using offline signatures */
  GNUNET_assert (GNUNET_CRYPTO_BSA_CS == bs_pub->cipher);
  if (GNUNET_OK !=
      TALER_config_get_amount (DH_cfg,
                               section_name,
                               "value",
                               &value))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "CS helper provided key for configuration section `%s' that has no `value' option set\n",
                section_name);
    return;
  }
  /* FIXME: could additionally sanity-check that this
     section actually has CIPHER = CS, etc. */

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "CS helper announces key %s for donation unit type %s with validity %s\n",
              GNUNET_h2s (&h_cs->hash),
              section_name,
              GNUNET_STRINGS_relative_time_to_string (validity_duration,
                                                      false));
  du = GNUNET_CONTAINER_multihashmap_get (du_keys,
                                          &h_cs->hash);
  if (NULL != du)
  {
    /* should be just an update (revocation!), so update existing entry */
    du->lost = GNUNET_TIME_relative_is_zero (validity_duration);
    return;
  }
  GNUNET_assert (NULL != sm_pub);
  check_donation_unit_cs_sm_pub (sm_pub);

  du = GNUNET_new (struct DH_DonationUnitKey);
  du->h_donation_unit_pub.hash = h_cs->hash;
  du->donation_unit_pub.bsign_pub_key
    = GNUNET_CRYPTO_bsign_pub_incref (bs_pub);
  du->validity_year = GNUNET_TIME_time_to_year (start_time.abs_time);
  du->value = value;
  du->lost = GNUNET_TIME_relative_is_zero (validity_duration);
  GNUNET_assert (
    GNUNET_OK ==
    GNUNET_CONTAINER_multihashmap_put (
      du_keys,
      &du->h_donation_unit_pub.hash,
      du,
      GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY));

  qs = DH_plugin->insert_donation_unit (
    DH_plugin->cls,
    &du->h_donation_unit_pub,
    &du->donation_unit_pub,
    du->validity_year,
    &du->value);
  if (qs < 0)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to insert donation units\n");
    GNUNET_SCHEDULER_shutdown ();
    DH_global_ret = EXIT_FAILURE;
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Inserted CS donation unit of %s\n",
              TALER_amount2s (&value));

  key_generation++;
}


/**
 * Function called with information about available keys for signing.  Usually
 * only called once per key upon connect. Also called again in case a key is
 * being revoked, in that case with an @a end_time of zero.
 *
 * @param cls NULL
 * @param start_time when does the key become available for signing;
 *                 zero if the key has been revoked or purged
 * @param validity_duration how long does the key remain available for signing;
 *                 zero if the key has been revoked or purged
 * @param donau_pub the public key itself, NULL if the key was revoked or purged
 * @param sm_pub public key of the security module, NULL if the key was revoked or purged
 * @param sm_sig signature from the security module
 */
static void
helper_esign_cb (
  void *cls,
  struct GNUNET_TIME_Timestamp start_time,
  struct GNUNET_TIME_Relative validity_duration,
  const struct TALER_ExchangePublicKeyP *donau_pub,
  const struct TALER_SecurityModulePublicKeyP *sm_pub,
  const struct TALER_SecurityModuleSignatureP *sm_sig)
{
  struct SigningKey *sk;
  struct GNUNET_PeerIdentity pid;
  unsigned long long expire_legal;
  /* need to "cast" because secmod works with TALER_ExchangePublicKeyP */
  struct DONAU_DonauPublicKeyP donau_pubkey = {
    .eddsa_pub = donau_pub->eddsa_pub
  };
  enum GNUNET_DB_QueryStatus qs;

  (void) cls;
  (void) sm_sig; /* not using offline signing */
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "EdDSA helper announces signing key %s with validity %s\n",
              TALER_B2S (donau_pub),
              GNUNET_STRINGS_relative_time_to_string (validity_duration,
                                                      false));

  pid.public_key = donau_pub->eddsa_pub;
  sk = GNUNET_CONTAINER_multipeermap_get (esign_keys,
                                          &pid);
  if (NULL != sk)
  {
    /* should be just an update (revocation!), so update existing entry */
    sk->lost = GNUNET_TIME_relative_is_zero (validity_duration);
    return;
  }
  GNUNET_assert (NULL != sm_pub);
  check_esign_sm_pub (sm_pub);

  sk = GNUNET_new (struct SigningKey);
  sk->donau_pub = donau_pubkey;
  sk->meta.valid_from = start_time;
  sk->meta.expire_sign
    = GNUNET_TIME_absolute_to_timestamp (
        GNUNET_TIME_absolute_add (start_time.abs_time,
                                  validity_duration));

  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_number (DH_cfg,
                                             "donau",
                                             "EXPIRE_LEGAL_YEARS",
                                             &expire_legal))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Need EXPIRE_LEGAL_YEARS in section `donau'\n");
    GNUNET_SCHEDULER_shutdown ();
    DH_global_ret = EXIT_FAILURE;
    return;
  }
  sk->meta.expire_legal
    = GNUNET_TIME_absolute_to_timestamp (
        GNUNET_TIME_absolute_add (start_time.abs_time,
                                  GNUNET_TIME_relative_multiply (
                                    GNUNET_TIME_UNIT_YEARS,
                                    expire_legal)));

  GNUNET_assert (
    GNUNET_OK ==
    GNUNET_CONTAINER_multipeermap_put (
      esign_keys,
      &pid,
      sk,
      GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY));

  qs = DH_plugin->insert_signing_key (
    DH_plugin->cls,
    &donau_pubkey,
    &sk->meta);
  if (qs < 0)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to insert donation units\n");
    GNUNET_SCHEDULER_shutdown ();
    DH_global_ret = EXIT_FAILURE;
    return;
  }

  key_generation++;
}


enum GNUNET_GenericReturnValue
DH_keys_init ()
{
  du_keys
    = GNUNET_CONTAINER_multihashmap_create (1024,
                                            GNUNET_YES);
  esign_keys
    = GNUNET_CONTAINER_multipeermap_create (32,
                                            GNUNET_NO /* MUST BE NO! */);
  rsadh = TALER_CRYPTO_helper_rsa_connect (DH_cfg,
                                           "donau",
                                           &helper_rsa_cb,
                                           NULL);
  if (NULL == rsadh)
  {
    GNUNET_break (0);
    DH_keys_finished ();
    return GNUNET_SYSERR;
  }
  csdh = TALER_CRYPTO_helper_cs_connect (DH_cfg,
                                         "donau",
                                         &helper_cs_cb,
                                         NULL);
  if (NULL == csdh)
  {
    GNUNET_break (0);
    DH_keys_finished ();
    return GNUNET_SYSERR;
  }
  esh = TALER_CRYPTO_helper_esign_connect (DH_cfg,
                                           "donau",
                                           &helper_esign_cb,
                                           NULL);
  if (NULL == esh)
  {
    GNUNET_break (0);
    DH_keys_finished ();
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


/**
 * Function called with information about the donau's donation_unit keys.
 *
 * @param cls NULL
 * @param donation_unit_pub public key of the donation_unit
 * @param h_donation_unit_pub hash of @a donation_unit_pub
 * @param validity_year of the donation unit
 * @param value of the donation unit
 */
static enum GNUNET_GenericReturnValue
donation_unit_info_cb (
  void *cls,
  const struct DONAU_DonationUnitHashP *h_donation_unit_pub,
  const struct DONAU_DonationUnitPublicKey *donation_unit_pub,
  uint64_t validity_year,
  struct TALER_Amount *value)
{
  struct DH_DonationUnitKey *du;

  (void) cls;
  GNUNET_assert (GNUNET_CRYPTO_BSA_INVALID !=
                 donation_unit_pub->bsign_pub_key->cipher);
  du = GNUNET_CONTAINER_multihashmap_get (du_keys,
                                          &h_donation_unit_pub->hash);
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Got %s key from database\n",
              NULL == du ? "unknown" : "known");
  if (NULL != du)
  {
    /* we already know this, nothing to do */
    return GNUNET_OK;
  }

  du = GNUNET_new (struct DH_DonationUnitKey);
  du->h_donation_unit_pub = *h_donation_unit_pub;
  DONAU_donation_unit_pub_deep_copy (&du->donation_unit_pub,
                                     donation_unit_pub);
  du->validity_year = validity_year;
  du->value = *value;
  du->lost = true; /* no private key known, that can only come from the helper! */
  GNUNET_assert (
    GNUNET_OK ==
    GNUNET_CONTAINER_multihashmap_put (du_keys,
                                       &du->h_donation_unit_pub.hash,
                                       du,
                                       GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)
    );
  return GNUNET_OK;
}


/**
 * Function called with information about the donau's online signing keys.
 *
 * @param cls NULL
 * @param donau_pub the public key
 * @param meta meta data information about the denomination type (expirations)
 */
static void
iterate_active_signing_keys_cb (
  void *cls,
  const struct DONAU_DonauPublicKeyP *donau_pub,
  struct DONAUDB_SignkeyMetaData *meta)
{
  /* The 'pid' is used as the key in the "peer" map... */
  struct GNUNET_PeerIdentity pid = {
    .public_key = donau_pub->eddsa_pub
  };
  struct SigningKey *sk;

  (void) cls;
  sk = GNUNET_CONTAINER_multipeermap_get (esign_keys,
                                          &pid);
  if (NULL != sk)
  {
    /* should be just an update (revocation!), so update existing entry */
    return;
  }
  sk = GNUNET_new (struct SigningKey);
  sk->donau_pub = *donau_pub;
  sk->meta = *meta;
  sk->lost = true;  /* no private key known, that can only come from the helper! */
  GNUNET_assert (
    GNUNET_OK ==
    GNUNET_CONTAINER_multipeermap_put (esign_keys,
                                       &pid,
                                       sk,
                                       GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)
    );
}


/**
 * Create a key state.
 *
 * @return NULL on error (i.e. failed to access database)
 */
static struct DH_KeyStateHandle *
build_key_state ()
{
  struct DH_KeyStateHandle *ksh;
  enum GNUNET_DB_QueryStatus qs;

  ksh = GNUNET_new (struct DH_KeyStateHandle);
  ksh->signature_expires = GNUNET_TIME_UNIT_FOREVER_TS;
  ksh->reload_time = GNUNET_TIME_timestamp_get ();
  /* We must use the key_generation from when we STARTED the process! */
  ksh->key_generation = key_generation;

  /* NOTE: fetches master-signed signkeys, but ALSO those that were revoked! */
  GNUNET_break (GNUNET_OK ==
                DH_plugin->preflight (DH_plugin->cls));
  qs = DH_plugin->iterate_donation_units (DH_plugin->cls,
                                          &donation_unit_info_cb,
                                          NULL);
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Fetched %d donation unit keys from DB\n",
              (int) qs);
  if (qs < 0)
  {
    GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
    GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs);
    destroy_key_state (ksh);
    return NULL;
  }

  /* NOTE: ONLY fetches active signkeys! */
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Fetching active signing keys from DB\n");
  qs = DH_plugin->iterate_active_signing_keys (DH_plugin->cls,
                                               &iterate_active_signing_keys_cb,
                                               NULL);
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Fetched %d active signing keys from DB\n",
              (int) qs);
  if (qs < 0)
  {
    GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
    GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs);
    destroy_key_state (ksh);
    return NULL;
  }

  if (GNUNET_OK !=
      finish_keys_response (ksh))
  {
    GNUNET_log (
      GNUNET_ERROR_TYPE_WARNING,
      "Could not finish /keys response (likely no signing keys available yet)\n");
    destroy_key_state (ksh);
    return NULL;
  }

  return ksh;
}


static struct DH_KeyStateHandle*
DH_keys_get_state ()
{
  struct DH_KeyStateHandle *old_ksh;
  struct DH_KeyStateHandle *ksh;

  old_ksh = key_state;
  if (NULL == old_ksh)
  {
    ksh = build_key_state ();
    if (NULL == ksh)
      return NULL;
    key_state = ksh;
    return ksh;
  }
  if ( (old_ksh->key_generation < key_generation) ||
       (GNUNET_TIME_absolute_is_past (old_ksh->signature_expires.abs_time)) )
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Rebuilding /keys, generation upgrade from %llu to %llu\n",
                (unsigned long long ) old_ksh->key_generation,
                (unsigned long long ) key_generation);
    ksh = build_key_state ();
    key_state = ksh;
    destroy_key_state (old_ksh);
    return ksh;
  }
  return old_ksh;
}


MHD_RESULT
DH_handler_keys (struct DH_RequestContext *rc,
                 const char *const args[])
{
  struct MHD_Connection *connection = rc->connection;
  struct DH_KeyStateHandle *ksh;
  struct MHD_Response *resp;

  (void) args;
  sync_key_helpers ();
  ksh = DH_keys_get_state ();
  if (NULL == ksh)
  {
    if ( (GNUNET_is_zero (&donation_unit_rsa_sm_pub)) &&
         (GNUNET_is_zero (&donation_unit_cs_sm_pub)) )
    {
      /* Either IPC failed, or neither helper had any donation_unit configured. */
      return TALER_MHD_reply_with_error (connection,
                                         MHD_HTTP_BAD_GATEWAY,
                                         TALER_EC_DONAU_DONATION_UNIT_HELPER_UNAVAILABLE,
                                         NULL);
    }
    if (GNUNET_is_zero (&esign_sm_pub))
    {
      return TALER_MHD_reply_with_error (connection,
                                         MHD_HTTP_BAD_GATEWAY,
                                         TALER_EC_DONAU_SIGNKEY_HELPER_UNAVAILABLE,
                                         NULL);
    }
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to build /keys response?\n");
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_SERVICE_UNAVAILABLE,
                                       TALER_EC_DONAU_GENERIC_KEYS_MISSING,
                                       "failed to create keys response");
  }
  resp = (TALER_MHD_CT_DEFLATE ==
          TALER_MHD_can_compress (connection,
                                  TALER_MHD_CT_DEFLATE))
    ? ksh->response_compressed
    : ksh->response_uncompressed;
  GNUNET_assert (NULL != resp);
  return MHD_queue_response (connection,
                             MHD_HTTP_OK,
                             resp);

}


enum TALER_ErrorCode
DH_keys_donau_sign_ (
  const struct GNUNET_CRYPTO_SignaturePurpose *purpose,
  struct DONAU_DonauPublicKeyP *pub,
  struct DONAU_DonauSignatureP *sig)
{
  struct DH_KeyStateHandle *ksh;
  enum TALER_ErrorCode ec;

  ksh = DH_keys_get_state ();
  if (NULL == ksh)
  {
    /* This *can* happen if the Donau's crypto helper is not running
       or had some bad error. */
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Cannot sign request, no valid signing keys available.\n");
    return TALER_EC_DONAU_GENERIC_KEYS_MISSING;
  }

  {
    /* need to "cast" because TALER_CRYPTO works with TALER_Exchange.. */
    struct TALER_ExchangePublicKeyP donau_pub;
    struct TALER_ExchangeSignatureP donau_sig;

    ec = TALER_CRYPTO_helper_esign_sign_ (esh,
                                          purpose,
                                          &donau_pub,
                                          &donau_sig);
    if (TALER_EC_NONE != ec)
      return ec;
    pub->eddsa_pub = donau_pub.eddsa_pub;
    sig->eddsa_sig = donau_sig.eddsa_signature;
  }
  return ec;
}


enum TALER_ErrorCode
DH_keys_donation_unit_batch_sign (
  unsigned int num_bkps,
  const struct DONAU_BkpSignData bkps[num_bkps],
  struct DONAU_BlindedDonationUnitSignature du_sigs[num_bkps])
{
  struct DH_KeyStateHandle *ksh;
  struct DH_DonationUnitKey *du;
  struct TALER_CRYPTO_RsaSignRequest rsrs[num_bkps];
  struct TALER_CRYPTO_CsSignRequest csrs[num_bkps];
  struct TALER_BlindedDenominationSignature rs[num_bkps];
  struct TALER_BlindedDenominationSignature cs[num_bkps];
  unsigned int rsrs_pos = 0;
  unsigned int csrs_pos = 0;
  enum TALER_ErrorCode ec;

  ksh = DH_keys_get_state ();
  if (NULL == ksh)
    return TALER_EC_DONAU_GENERIC_KEYS_MISSING;
  for (unsigned int i = 0; i<num_bkps; i++)
  {
    const struct DONAU_DonationUnitHashP *h_du_pub =
      bkps[i].h_donation_unit_pub;
    const struct DONAU_BlindedUniqueDonorIdentifier *budi = bkps[i].budi;

    du = GNUNET_CONTAINER_multihashmap_get (du_keys,
                                            &h_du_pub->hash);
    if (NULL == du)
      return TALER_EC_DONAU_GENERIC_DONATION_UNIT_UNKNOWN;
    if (budi->blinded_message->cipher !=
        du->donation_unit_pub.bsign_pub_key->cipher)
      return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
    switch (du->donation_unit_pub.bsign_pub_key->cipher)
    {
    case GNUNET_CRYPTO_BSA_RSA:
      /* See DONAU_donation_unit_pub_hash: we guarantee that these
         hashes are equivalent! */
      rsrs[rsrs_pos].h_rsa
        = (const struct TALER_RsaPubHashP *) &du->h_donation_unit_pub;
      rsrs[rsrs_pos].msg
        = budi->blinded_message->details.rsa_blinded_message.blinded_msg;
      rsrs[rsrs_pos].msg_size
        = budi->blinded_message->details.rsa_blinded_message.blinded_msg_size;
      rsrs_pos++;
      break;
    case GNUNET_CRYPTO_BSA_CS:
      /* See DONAU_donation_unit_pub_hash: we guarantee that these
         hashes are equivalent! */
      csrs[csrs_pos].h_cs
        = (const struct TALER_CsPubHashP *) &du->h_donation_unit_pub;
      csrs[csrs_pos].blinded_planchet
        = &budi->blinded_message->details.cs_blinded_message;
      csrs_pos++;
      break;
    default:
      return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
    }
  }

  if ( (0 != csrs_pos) &&
       (0 != rsrs_pos) )
  {
    memset (rs,
            0,
            sizeof (rs));
    memset (cs,
            0,
            sizeof (cs));
  }
  ec = TALER_EC_NONE;
  if (0 != csrs_pos)
  {
    ec = TALER_CRYPTO_helper_cs_batch_sign (
      csdh,
      csrs_pos,
      csrs,
      false, // for_melt
      cs);
    if (TALER_EC_NONE != ec)
    {
      for (unsigned int i = 0; i<csrs_pos; i++)
      {
        if (NULL != cs[i].blinded_sig)
        {
          GNUNET_CRYPTO_blinded_sig_decref (cs[i].blinded_sig);
          cs[i].blinded_sig = NULL;
        }
      }
      return ec;
    }
  }
  if (0 != rsrs_pos)
  {
    ec = TALER_CRYPTO_helper_rsa_batch_sign (
      rsadh,
      rsrs_pos,
      rsrs,
      rs);
    if (TALER_EC_NONE != ec)
    {
      for (unsigned int i = 0; i<csrs_pos; i++)
      {
        if (NULL != cs[i].blinded_sig)
        {
          GNUNET_CRYPTO_blinded_sig_decref (cs[i].blinded_sig);
          cs[i].blinded_sig = NULL;
        }
      }
      for (unsigned int i = 0; i<rsrs_pos; i++)
      {
        if (NULL != rs[i].blinded_sig)
        {
          GNUNET_CRYPTO_blinded_sig_decref (rs[i].blinded_sig);
          rs[i].blinded_sig = NULL;
        }
      }
      return ec;
    }
  }

  rsrs_pos = 0;
  csrs_pos = 0;
  for (unsigned int i = 0; i<num_bkps; i++)
  {
    const struct DONAU_BlindedUniqueDonorIdentifier *budi = bkps[i].budi;

    switch (budi->blinded_message->cipher)
    {
    case GNUNET_CRYPTO_BSA_RSA:
      du_sigs[i].blinded_sig = rs[rsrs_pos++].blinded_sig;
      break;
    case GNUNET_CRYPTO_BSA_CS:
      du_sigs[i].blinded_sig = cs[csrs_pos++].blinded_sig;
      break;
    default:
      GNUNET_assert (0);
    }
  }
  return TALER_EC_NONE;
}


struct DH_DonationUnitKey *
DH_keys_donation_unit_by_hash (
  const struct DONAU_DonationUnitHashP *h_du_pub)
{
  return GNUNET_CONTAINER_multihashmap_get (du_keys,
                                            &h_du_pub->hash);
}


enum TALER_ErrorCode
DH_keys_donation_unit_cs_r_pub (
  const struct DONAU_DonationUnitHashP *h_donation_unit_pub,
  const struct GNUNET_CRYPTO_CsSessionNonce *nonce,
  struct GNUNET_CRYPTO_CSPublicRPairP *r_pub)
{
  struct DH_DonationUnitKey *dk;

  dk = DH_keys_donation_unit_by_hash (h_donation_unit_pub);
  if (NULL == dk)
  {
    return TALER_EC_DONAU_GENERIC_DONATION_UNIT_UNKNOWN;
  }
  if (GNUNET_CRYPTO_BSA_CS !=
      dk->donation_unit_pub.bsign_pub_key->cipher)
  {
    return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
  }

  {
    struct TALER_CRYPTO_CsDeriveRequest cdr = {
      .h_cs = (const struct TALER_CsPubHashP *) &dk->h_donation_unit_pub,
      .nonce = nonce
    };
    return TALER_CRYPTO_helper_cs_r_derive (csdh,
                                            &cdr,
                                            false,
                                            r_pub);
  }
}


/* end of donau-httpd_keys.c */
