merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

taler-merchant-httpd_token-keys.c (29817B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2024-2026 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file src/backend/taler-merchant-httpd_token-keys.c
     18  * @brief shared token family key lookup, lifetime extension and creation
     19  *        for orders and fountains; also provides fountain key windows
     20  * @author Christian Blättler
     21  * @author Bohdan Potuzhnyi
     22  */
     23 #include "platform.h"
     24 #include "taler-merchant-httpd_token-keys.h"
     25 #include "merchant-database/insert_token_family_key.h"
     26 #include "merchant-database/update_token_family_key_expiration.h"
     27 #include "merchant-database/start.h"
     28 
     29 
     30 /**
     31  * Get rounded time interval. @a start is calculated by rounding
     32  * @a ts down to the nearest multiple of @a precision.
     33  *
     34  * @param precision rounding precision.
     35  *        year, month, day, hour, minute are supported.
     36  * @param ts timestamp to round
     37  * @param[out] start start of the interval
     38  * @return #GNUNET_OK on success, #GNUNET_SYSERR on error
     39  */
     40 static enum GNUNET_GenericReturnValue
     41 get_rounded_time_interval_down (struct GNUNET_TIME_Relative precision,
     42                                 struct GNUNET_TIME_Timestamp ts,
     43                                 struct GNUNET_TIME_Timestamp *start)
     44 {
     45   enum GNUNET_TIME_RounderInterval ri;
     46 
     47   ri = GNUNET_TIME_relative_to_round_interval (precision);
     48   if ( (GNUNET_TIME_RI_NONE == ri) &&
     49        (! GNUNET_TIME_relative_is_zero (precision)) )
     50   {
     51     *start = ts;
     52     return GNUNET_SYSERR;
     53   }
     54   *start = GNUNET_TIME_absolute_to_timestamp (
     55     GNUNET_TIME_round_down (ts.abs_time,
     56                             ri));
     57   return GNUNET_OK;
     58 }
     59 
     60 
     61 /**
     62  * Get rounded time interval. @a start is calculated by rounding
     63  * @a ts up to the nearest multiple of @a precision.
     64  *
     65  * @param precision rounding precision.
     66  *        year, month, day, hour, minute are supported.
     67  * @param ts timestamp to round
     68  * @param[out] start start of the interval
     69  * @return #GNUNET_OK on success, #GNUNET_SYSERR on error
     70  */
     71 static enum GNUNET_GenericReturnValue
     72 get_rounded_time_interval_up (struct GNUNET_TIME_Relative precision,
     73                               struct GNUNET_TIME_Timestamp ts,
     74                               struct GNUNET_TIME_Timestamp *start)
     75 {
     76   enum GNUNET_TIME_RounderInterval ri;
     77 
     78   ri = GNUNET_TIME_relative_to_round_interval (precision);
     79   if ( (GNUNET_TIME_RI_NONE == ri) &&
     80        (! GNUNET_TIME_relative_is_zero (precision)) )
     81   {
     82     *start = ts;
     83     return GNUNET_SYSERR;
     84   }
     85   *start = GNUNET_TIME_absolute_to_timestamp (
     86     GNUNET_TIME_round_up (ts.abs_time,
     87                           ri));
     88   return GNUNET_OK;
     89 }
     90 
     91 
     92 /**
     93  * Create fresh key pair based on @a cipher_spec.
     94  *
     95  * @param cipher_spec which kind of key pair should we generate
     96  * @param[out] priv set to new private key
     97  * @param[out] pub set to new public key
     98  * @return #GNUNET_OK on success
     99  */
    100 static enum GNUNET_GenericReturnValue
    101 create_key (const char *cipher_spec,
    102             struct TALER_TokenIssuePrivateKey *priv,
    103             struct TALER_TokenIssuePublicKey *pub)
    104 {
    105   unsigned int len;
    106   char dummy;
    107 
    108   if (0 == strcmp ("cs",
    109                    cipher_spec))
    110   {
    111     GNUNET_CRYPTO_blind_sign_keys_create (
    112       &priv->private_key,
    113       &pub->public_key,
    114       GNUNET_CRYPTO_BSA_CS);
    115     return GNUNET_OK;
    116   }
    117   if (1 ==
    118       sscanf (cipher_spec,
    119               "rsa(%u)%c",
    120               &len,
    121               &dummy))
    122   {
    123     GNUNET_CRYPTO_blind_sign_keys_create (
    124       &priv->private_key,
    125       &pub->public_key,
    126       GNUNET_CRYPTO_BSA_RSA,
    127       len);
    128     return GNUNET_OK;
    129   }
    130   return GNUNET_SYSERR;
    131 }
    132 
    133 
    134 void
    135 TMH_token_key_details_free (
    136   struct TALER_MERCHANTDB_TokenFamilyKeyDetails *key_details)
    137 {
    138   GNUNET_free (key_details->token_family.slug);
    139   GNUNET_free (key_details->token_family.name);
    140   GNUNET_free (key_details->token_family.description);
    141   json_decref (key_details->token_family.description_i18n);
    142   key_details->token_family.description_i18n = NULL;
    143   json_decref (key_details->token_family.extra_data);
    144   key_details->token_family.extra_data = NULL;
    145   GNUNET_free (key_details->token_family.cipher_spec);
    146   if (NULL != key_details->pub.public_key)
    147   {
    148     GNUNET_CRYPTO_blind_sign_pub_decref (key_details->pub.public_key);
    149     key_details->pub.public_key = NULL;
    150   }
    151   if (NULL != key_details->priv.private_key)
    152   {
    153     GNUNET_CRYPTO_blind_sign_priv_decref (key_details->priv.private_key);
    154     key_details->priv.private_key = NULL;
    155   }
    156 }
    157 
    158 
    159 /**
    160  * Create, store and return a fresh issue key covering @a valid_at
    161  * for the token family described by @a key_details.
    162  *
    163  * @param connection connection to report errors on
    164  * @param instance_id instance owning the token family
    165  * @param slug slug of the token family
    166  * @param valid_at time the new key must cover
    167  * @param sign_until how long the private key must remain usable
    168  * @param require_coverage if true, leave the key unset when the existing
    169  *        validity rules cannot cover @a valid_at within the family bounds
    170  * @param[in,out] key_details token family details on entry; the new
    171  *        key and its validity bounds are added on success
    172  * @return #GNUNET_OK on success, #GNUNET_NO if an error response was
    173  *         queued, #GNUNET_SYSERR on hard failure
    174  */
    175 static enum GNUNET_GenericReturnValue
    176 mint_key (struct MHD_Connection *connection,
    177           const char *instance_id,
    178           const char *slug,
    179           struct GNUNET_TIME_Timestamp valid_at,
    180           struct GNUNET_TIME_Timestamp sign_until,
    181           bool require_coverage,
    182           struct TALER_MERCHANTDB_TokenFamilyKeyDetails *key_details)
    183 {
    184   struct TALER_MERCHANT_ContractTokenFamilyKey key;
    185   enum GNUNET_DB_QueryStatus iqs;
    186   struct TALER_TokenIssuePrivateKey token_priv;
    187   struct GNUNET_TIME_Timestamp key_expires;
    188   struct GNUNET_TIME_Timestamp round_start;
    189 
    190   /* Offset the rounded period once. Subtracting the offset before rounding
    191      as well can select an already expired period, even when duration is at
    192      least granularity + offset. Existing covering keys are reused by lookup. */
    193   if (GNUNET_OK !=
    194       get_rounded_time_interval_down (
    195         key_details->token_family.validity_granularity,
    196         valid_at,
    197         &round_start))
    198   {
    199     GNUNET_break (0);
    200     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    201                 "Unsupported validity granularity interval %s found in database for token family %s!\n",
    202                 GNUNET_TIME_relative2s (
    203                   key_details->token_family.validity_granularity,
    204                   false),
    205                 slug);
    206     TMH_token_key_details_free (key_details);
    207     return (MHD_YES ==
    208             TALER_MHD_reply_with_error (connection,
    209                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    210                                         TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
    211                                         "get_rounded_time_interval_down failed"))
    212            ? GNUNET_NO
    213            : GNUNET_SYSERR;
    214   }
    215   if (GNUNET_TIME_relative_cmp (
    216         key_details->token_family.duration,
    217         <,
    218         GNUNET_TIME_relative_add (
    219           key_details->token_family.validity_granularity,
    220           key_details->token_family.start_offset)))
    221   {
    222     GNUNET_break (0);
    223     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    224                 "Inconsistent duration %s found in database for token family %s (below validity granularity plus start_offset)!\n",
    225                 GNUNET_TIME_relative2s (key_details->token_family.duration,
    226                                         false),
    227                 slug);
    228     TMH_token_key_details_free (key_details);
    229     return (MHD_YES ==
    230             TALER_MHD_reply_with_error (connection,
    231                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    232                                         TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
    233                                         "duration, validity_granularity and start_offset inconsistent for token family"))
    234            ? GNUNET_NO
    235            : GNUNET_SYSERR;
    236   }
    237   key.valid_after
    238     = GNUNET_TIME_timestamp_max (
    239         GNUNET_TIME_absolute_to_timestamp (
    240           GNUNET_TIME_absolute_subtract (
    241             round_start.abs_time,
    242             key_details->token_family.start_offset)),
    243         key_details->token_family.valid_after);
    244   key.valid_before
    245     = GNUNET_TIME_timestamp_min (
    246         GNUNET_TIME_absolute_to_timestamp (
    247           GNUNET_TIME_absolute_add (
    248             key.valid_after.abs_time,
    249             key_details->token_family.duration)),
    250         key_details->token_family.valid_before);
    251   GNUNET_assert (GNUNET_OK ==
    252                  get_rounded_time_interval_down (
    253                    key_details->token_family.validity_granularity,
    254                    key.valid_before,
    255                    &key_expires));
    256   /* Make sure key never expires before @a sign_until */
    257   key_expires = GNUNET_TIME_timestamp_max (
    258     sign_until,
    259     key_expires);
    260   if (GNUNET_TIME_timestamp_cmp (
    261         key_expires,
    262         ==,
    263         round_start))
    264   {
    265     /* valid_before does not actually end after the
    266        next rounded validity period would start;
    267        determine next rounded validity period
    268        start point and extend valid_before to cover
    269        the full validity period */
    270     GNUNET_assert (
    271       GNUNET_OK ==
    272       get_rounded_time_interval_up (
    273         key_details->token_family.validity_granularity,
    274         key.valid_before,
    275         &key_expires));
    276     /* This should basically always end up being key_expires */
    277     key.valid_before = GNUNET_TIME_timestamp_max (key.valid_before,
    278                                                   key_expires);
    279   }
    280   /* Fountains must not advertise a key that fails to advance the
    281      coverage chain. Check before generating or storing anything. The
    282      order path retains its existing validity semantics. */
    283   if (require_coverage &&
    284       (GNUNET_TIME_timestamp_cmp (key.valid_after, >, valid_at) ||
    285        GNUNET_TIME_timestamp_cmp (key.valid_before, <, valid_at) ||
    286        GNUNET_TIME_timestamp_cmp (key.valid_before,
    287                                   >,
    288                                   key_details->token_family.valid_before)))
    289     return GNUNET_OK;
    290   if (GNUNET_OK !=
    291       create_key (key_details->token_family.cipher_spec,
    292                   &token_priv,
    293                   &key.pub))
    294   {
    295     GNUNET_break (0);
    296     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    297                 "Unsupported cipher family %s found in database for token family %s!\n",
    298                 key_details->token_family.cipher_spec,
    299                 slug);
    300     TMH_token_key_details_free (key_details);
    301     return (MHD_YES ==
    302             TALER_MHD_reply_with_error (connection,
    303                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    304                                         TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
    305                                         "invalid cipher stored in local database for token family"))
    306            ? GNUNET_NO
    307            : GNUNET_SYSERR;
    308   }
    309   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    310               "Storing new key for slug %s of %s\n",
    311               slug,
    312               instance_id);
    313   iqs = TALER_MERCHANTDB_insert_token_family_key (TMH_db,
    314                                                   instance_id,
    315                                                   slug,
    316                                                   &key.pub,
    317                                                   &token_priv,
    318                                                   key_expires,
    319                                                   key.valid_after,
    320                                                   key.valid_before);
    321   switch (iqs)
    322   {
    323   case GNUNET_DB_STATUS_HARD_ERROR:
    324     GNUNET_break (0);
    325     GNUNET_CRYPTO_blind_sign_priv_decref (token_priv.private_key);
    326     GNUNET_CRYPTO_blind_sign_pub_decref (key.pub.public_key);
    327     TMH_token_key_details_free (key_details);
    328     return (MHD_YES ==
    329             TALER_MHD_reply_with_error (connection,
    330                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    331                                         TALER_EC_GENERIC_DB_STORE_FAILED,
    332                                         NULL))
    333            ? GNUNET_NO
    334            : GNUNET_SYSERR;
    335   case GNUNET_DB_STATUS_SOFT_ERROR:
    336     /* Report the conflict without advertising an uncommitted key. */
    337     GNUNET_break (0);
    338     GNUNET_CRYPTO_blind_sign_priv_decref (token_priv.private_key);
    339     GNUNET_CRYPTO_blind_sign_pub_decref (key.pub.public_key);
    340     TMH_token_key_details_free (key_details);
    341     return (MHD_YES ==
    342             TALER_MHD_reply_with_error (connection,
    343                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    344                                         TALER_EC_GENERIC_DB_SOFT_FAILURE,
    345                                         NULL))
    346            ? GNUNET_NO
    347            : GNUNET_SYSERR;
    348   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    349     GNUNET_break (0);
    350     GNUNET_CRYPTO_blind_sign_priv_decref (token_priv.private_key);
    351     GNUNET_CRYPTO_blind_sign_pub_decref (key.pub.public_key);
    352     TMH_token_key_details_free (key_details);
    353     return (MHD_YES ==
    354             TALER_MHD_reply_with_error (connection,
    355                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    356                                         TALER_EC_GENERIC_DB_STORE_FAILED,
    357                                         NULL))
    358            ? GNUNET_NO
    359            : GNUNET_SYSERR;
    360   case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    361     break;
    362   }
    363   key_details->pub = key.pub;
    364   key_details->priv = token_priv;
    365   key_details->signature_validity_start = key.valid_after;
    366   key_details->signature_validity_end = key.valid_before;
    367   key_details->private_key_deleted_at = key_expires;
    368   return GNUNET_OK;
    369 }
    370 
    371 
    372 enum GNUNET_GenericReturnValue
    373 TMH_token_key_extend (
    374   struct MHD_Connection *connection,
    375   const char *instance_id,
    376   const char *slug,
    377   struct GNUNET_TIME_Timestamp valid_at,
    378   struct GNUNET_TIME_Timestamp sign_until)
    379 {
    380   enum GNUNET_DB_QueryStatus qs;
    381 
    382   /* We are about to promise a token of this family, so the private key
    383      covering @a valid_at must survive until we sign at @a sign_until.  If
    384      an existing key covers the validity period but was minted for an order
    385      with an earlier pay deadline, extend its lifetime instead of minting a
    386      second key for the very same validity period: the key lookup below
    387      would otherwise consider that key missing and we would end up with two
    388      keys for one validity period.  */
    389   qs = TALER_MERCHANTDB_update_token_family_key_expiration (
    390     TMH_db,
    391     instance_id,
    392     slug,
    393     valid_at,
    394     sign_until);
    395   switch (qs)
    396   {
    397   case GNUNET_DB_STATUS_HARD_ERROR:
    398     GNUNET_break (0);
    399     return (MHD_YES ==
    400             TALER_MHD_reply_with_error (connection,
    401                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    402                                         TALER_EC_GENERIC_DB_STORE_FAILED,
    403                                         "update_token_family_key_expiration"))
    404            ? GNUNET_NO
    405            : GNUNET_SYSERR;
    406   case GNUNET_DB_STATUS_SOFT_ERROR:
    407     /* Report the conflict without advertising an uncommitted key. */
    408     GNUNET_break (0);
    409     return (MHD_YES ==
    410             TALER_MHD_reply_with_error (connection,
    411                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    412                                         TALER_EC_GENERIC_DB_SOFT_FAILURE,
    413                                         "update_token_family_key_expiration"))
    414            ? GNUNET_NO
    415            : GNUNET_SYSERR;
    416   default:
    417     /* No key needed extending, or one/more were extended; either is fine. */
    418     break;
    419   }
    420   return GNUNET_OK;
    421 }
    422 
    423 
    424 /**
    425  * Implementation of #TMH_token_key_ensure(). With @a require_coverage,
    426  * success may leave the key unset when rounding would fail to cover the
    427  * requested time. This allows fountains to stop without storing unusable
    428  * keys; the caller must still free the returned family details. The caller
    429  * must hold the token family lock throughout this operation.
    430  */
    431 static enum GNUNET_GenericReturnValue
    432 ensure_key_locked (
    433   struct MHD_Connection *connection,
    434   const char *instance_id,
    435   const char *slug,
    436   struct GNUNET_TIME_Timestamp valid_at,
    437   struct GNUNET_TIME_Timestamp sign_until,
    438   bool require_coverage,
    439   struct TALER_MERCHANTDB_TokenFamilyKeyDetails *key_details,
    440   bool *minted)
    441 {
    442   enum GNUNET_DB_QueryStatus qs;
    443   enum GNUNET_GenericReturnValue res;
    444 
    445   *minted = false;
    446   res = TMH_token_key_extend (connection,
    447                               instance_id,
    448                               slug,
    449                               valid_at,
    450                               sign_until);
    451   if (GNUNET_OK != res)
    452     return res;
    453   qs = TALER_MERCHANTDB_get_token_family_key (
    454     TMH_db,
    455     instance_id,
    456     slug,
    457     valid_at,
    458     sign_until,
    459     key_details);
    460   switch (qs)
    461   {
    462   case GNUNET_DB_STATUS_HARD_ERROR:
    463     GNUNET_break (0);
    464     return (MHD_YES ==
    465             TALER_MHD_reply_with_error (connection,
    466                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    467                                         TALER_EC_GENERIC_DB_FETCH_FAILED,
    468                                         "get_token_family_key"))
    469            ? GNUNET_NO
    470            : GNUNET_SYSERR;
    471   case GNUNET_DB_STATUS_SOFT_ERROR:
    472     /* Report the conflict without advertising an uncommitted key. */
    473     GNUNET_break (0);
    474     return (MHD_YES ==
    475             TALER_MHD_reply_with_error (connection,
    476                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    477                                         TALER_EC_GENERIC_DB_SOFT_FAILURE,
    478                                         "get_token_family_key"))
    479            ? GNUNET_NO
    480            : GNUNET_SYSERR;
    481   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    482     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    483                 "Token family slug %s unknown at %llu for %llu for instance %s\n",
    484                 slug,
    485                 (unsigned long long) valid_at.abs_time.abs_value_us,
    486                 (unsigned long long) sign_until.abs_time.abs_value_us,
    487                 instance_id);
    488     return (MHD_YES ==
    489             TALER_MHD_reply_with_error (connection,
    490                                         MHD_HTTP_NOT_FOUND,
    491                                         TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN,
    492                                         slug))
    493            ? GNUNET_NO
    494            : GNUNET_SYSERR;
    495   case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    496     break;
    497   }
    498 
    499   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    500               "Lookup of token family %s at %llu yielded %s\n",
    501               slug,
    502               (unsigned long long) valid_at.abs_time.abs_value_us,
    503               NULL == key_details->pub.public_key ? "no key" : "a key");
    504 
    505   if (NULL != key_details->pub.public_key)
    506     return GNUNET_OK;
    507 
    508   /* No suitable key exists, create one! */
    509   {
    510     res = mint_key (connection,
    511                     instance_id,
    512                     slug,
    513                     valid_at,
    514                     sign_until,
    515                     require_coverage,
    516                     key_details);
    517     if (GNUNET_OK != res)
    518       return res;
    519     *minted = (NULL != key_details->pub.public_key);
    520   }
    521   return GNUNET_OK;
    522 }
    523 
    524 
    525 /**
    526  * Hold the family lock across the entire lookup-and-mint operation. Only
    527  * commit transactions we started; withdrawal keeps the lock until its quota
    528  * and signatures are committed together.
    529  */
    530 static enum GNUNET_GenericReturnValue
    531 ensure_key (
    532   struct MHD_Connection *connection,
    533   const char *instance_id,
    534   const char *slug,
    535   struct GNUNET_TIME_Timestamp valid_at,
    536   struct GNUNET_TIME_Timestamp sign_until,
    537   bool require_coverage,
    538   struct TALER_MERCHANTDB_TokenFamilyKeyDetails *key_details,
    539   bool *minted)
    540 {
    541   bool started_transaction;
    542   enum GNUNET_DB_QueryStatus qs;
    543   enum GNUNET_GenericReturnValue res;
    544 
    545   *minted = false;
    546   qs = TALER_MERCHANTDB_lock_token_family (TMH_db,
    547                                            slug,
    548                                            &started_transaction);
    549   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
    550   {
    551     if (started_transaction)
    552       TALER_MERCHANTDB_rollback (TMH_db);
    553     return (MHD_YES ==
    554             TALER_MHD_reply_with_error (
    555               connection,
    556               MHD_HTTP_NOT_FOUND,
    557               TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN,
    558               slug))
    559       ? GNUNET_NO
    560       : GNUNET_SYSERR;
    561   }
    562   if (qs < 0)
    563   {
    564     if (started_transaction)
    565       TALER_MERCHANTDB_rollback (TMH_db);
    566     return (MHD_YES ==
    567             TALER_MHD_reply_with_error (
    568               connection,
    569               MHD_HTTP_INTERNAL_SERVER_ERROR,
    570               GNUNET_DB_STATUS_SOFT_ERROR == qs
    571               ? TALER_EC_GENERIC_DB_SOFT_FAILURE
    572               : TALER_EC_GENERIC_DB_STORE_FAILED,
    573               "lock_token_family"))
    574       ? GNUNET_NO
    575       : GNUNET_SYSERR;
    576   }
    577   res = ensure_key_locked (connection,
    578                            instance_id,
    579                            slug,
    580                            valid_at,
    581                            sign_until,
    582                            require_coverage,
    583                            key_details,
    584                            minted);
    585   if (! started_transaction)
    586     return res;
    587   if (GNUNET_OK != res)
    588   {
    589     TALER_MERCHANTDB_rollback (TMH_db);
    590     return res;
    591   }
    592   qs = TALER_MERCHANTDB_commit (TMH_db);
    593   if (qs >= 0)
    594     return GNUNET_OK;
    595   TMH_token_key_details_free (key_details);
    596   *minted = false;
    597   return (MHD_YES ==
    598           TALER_MHD_reply_with_error (
    599             connection,
    600             MHD_HTTP_INTERNAL_SERVER_ERROR,
    601             GNUNET_DB_STATUS_SOFT_ERROR == qs
    602             ? TALER_EC_GENERIC_DB_SOFT_FAILURE
    603             : TALER_EC_GENERIC_DB_COMMIT_FAILED,
    604             "ensure token family key"))
    605     ? GNUNET_NO
    606     : GNUNET_SYSERR;
    607 }
    608 
    609 
    610 enum GNUNET_GenericReturnValue
    611 TMH_token_key_ensure (
    612   struct MHD_Connection *connection,
    613   const char *instance_id,
    614   const char *slug,
    615   struct GNUNET_TIME_Timestamp valid_at,
    616   struct GNUNET_TIME_Timestamp sign_until,
    617   struct TALER_MERCHANTDB_TokenFamilyKeyDetails *key_details,
    618   bool *minted)
    619 {
    620   return ensure_key (connection,
    621                      instance_id,
    622                      slug,
    623                      valid_at,
    624                      sign_until,
    625                      false,
    626                      key_details,
    627                      minted);
    628 }
    629 
    630 
    631 void
    632 TMH_token_key_window_free (struct TMH_TokenKeyWindow *window)
    633 {
    634   for (unsigned int i = 0; i < window->keys_len; i++)
    635     TMH_token_key_details_free (&window->keys[i]);
    636   GNUNET_array_grow (window->keys,
    637                      window->keys_len,
    638                      0);
    639 }
    640 
    641 
    642 enum GNUNET_GenericReturnValue
    643 TMH_token_key_window_get (
    644   struct MHD_Connection *connection,
    645   const char *instance_id,
    646   const struct TALER_MERCHANTDB_TokenFamilyDetails *tf,
    647   struct GNUNET_TIME_Timestamp now,
    648   unsigned int key_window_size,
    649   struct TMH_TokenKeyWindow *window)
    650 {
    651   struct GNUNET_TIME_Timestamp cursor;
    652   struct GNUNET_TIME_Timestamp latest_start;
    653 
    654   *window = (struct TMH_TokenKeyWindow) {0};
    655   GNUNET_assert (key_window_size <= TMH_MAX_FOUNTAIN_KEY_WINDOW);
    656   latest_start = GNUNET_TIME_absolute_to_timestamp (
    657     GNUNET_TIME_absolute_add (
    658       now.abs_time,
    659       GNUNET_TIME_relative_multiply (tf->duration,
    660                                      key_window_size)));
    661   cursor = GNUNET_TIME_timestamp_max (now,
    662                                       tf->valid_after);
    663   /* A future family must fit the horizon measured from now; its start
    664      must never move that horizon forward. */
    665   if (GNUNET_TIME_timestamp_cmp (cursor, >, latest_start))
    666     return GNUNET_OK;
    667   for (unsigned int slot = 0; slot <= key_window_size; slot++)
    668   {
    669     struct TALER_MERCHANTDB_TokenFamilyKeyDetails kd;
    670     struct GNUNET_TIME_Timestamp next;
    671     enum GNUNET_GenericReturnValue res;
    672     bool minted;
    673 
    674     if (GNUNET_TIME_timestamp_cmp (cursor, >=, tf->valid_before))
    675       break;
    676     res = ensure_key (connection,
    677                       instance_id,
    678                       tf->slug,
    679                       cursor,
    680                       now,
    681                       true,
    682                       &kd,
    683                       &minted);
    684     if (GNUNET_OK != res)
    685     {
    686       TMH_token_key_window_free (window);
    687       return res;
    688     }
    689     if ( (NULL == kd.pub.public_key) ||
    690          GNUNET_TIME_timestamp_cmp (kd.signature_validity_start, >, latest_start) ||
    691          GNUNET_TIME_timestamp_cmp (kd.signature_validity_start, >, cursor) ||
    692          GNUNET_TIME_timestamp_cmp (kd.signature_validity_end, <, cursor) ||
    693          GNUNET_TIME_timestamp_cmp (kd.signature_validity_end,
    694                                     >,
    695                                     tf->valid_before) )
    696     {
    697       /* Rounding/offsets may not yield further coverage. Do not count
    698          a repeated or unusable period as another future key. */
    699       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    700                   "Token family %s cannot extend fountain coverage at %llu\n",
    701                   tf->slug,
    702                   (unsigned long long) cursor.abs_time.abs_value_us);
    703       TMH_token_key_details_free (&kd);
    704       break;
    705     }
    706     GNUNET_array_append (window->keys,
    707                          window->keys_len,
    708                          kd);
    709     /* Lookup includes the expiry instant. Move one whole timestamp
    710        tick beyond it, so the previous key cannot be selected again. */
    711     next = GNUNET_TIME_absolute_to_timestamp (
    712       GNUNET_TIME_absolute_add (kd.signature_validity_end.abs_time,
    713                                 GNUNET_TIME_UNIT_SECONDS));
    714     next = GNUNET_TIME_timestamp_min (next,
    715                                       latest_start);
    716     if (GNUNET_TIME_timestamp_cmp (next, <=, kd.signature_validity_end))
    717       break; /* reached the horizon, forever or saturated arithmetic */
    718     cursor = next;
    719   }
    720   return GNUNET_OK;
    721 }
    722 
    723 
    724 enum GNUNET_GenericReturnValue
    725 TMH_token_key_window_find (
    726   const struct TMH_TokenKeyWindow *window,
    727   struct GNUNET_TIME_Timestamp valid_at,
    728   unsigned int *key_index)
    729 {
    730   /* Wallets can select an advertised key unambiguously by its start,
    731      even if an earlier key also covers that instant. */
    732   for (unsigned int i = 0; i < window->keys_len; i++)
    733     if (GNUNET_TIME_timestamp_cmp (valid_at,
    734                                    ==,
    735                                    window->keys[i].signature_validity_start))
    736     {
    737       *key_index = i;
    738       return GNUNET_OK;
    739     }
    740   /* Otherwise keep the covering-key convention: first matching key. */
    741   for (unsigned int i = 0; i < window->keys_len; i++)
    742     if (GNUNET_TIME_timestamp_cmp (valid_at,
    743                                    >=,
    744                                    window->keys[i].signature_validity_start) &&
    745         GNUNET_TIME_timestamp_cmp (valid_at,
    746                                    <=,
    747                                    window->keys[i].signature_validity_end))
    748     {
    749       *key_index = i;
    750       return GNUNET_OK;
    751     }
    752   return GNUNET_NO;
    753 }
    754 
    755 
    756 void
    757 TMH_token_family_to_contract (
    758   const struct TALER_MERCHANTDB_TokenFamilyDetails *tf,
    759   struct TALER_MERCHANT_ContractTokenFamily *family)
    760 {
    761   struct TALER_MERCHANT_ContractTokenFamily new_family = {
    762     .slug = GNUNET_strdup (tf->slug),
    763     .name = GNUNET_strdup (tf->name),
    764     .description = GNUNET_strdup (tf->description),
    765     .description_i18n = json_incref (tf->description_i18n),
    766   };
    767 
    768   switch (tf->kind)
    769   {
    770   case TALER_MERCHANTDB_TFK_Subscription:
    771     {
    772       json_t *tdomains = json_object_get (tf->extra_data,
    773                                           "trusted_domains");
    774       json_t *dom;
    775       size_t i;
    776 
    777       new_family.kind = TALER_MERCHANT_CONTRACT_TOKEN_KIND_SUBSCRIPTION;
    778       new_family.critical = true;
    779       new_family.details.subscription.trusted_domains_len
    780         = json_array_size (tdomains);
    781       GNUNET_assert (new_family.details.subscription.trusted_domains_len
    782                      < UINT_MAX);
    783       new_family.details.subscription.trusted_domains
    784         = GNUNET_new_array (
    785             new_family.details.subscription.trusted_domains_len,
    786             char *);
    787       json_array_foreach (tdomains, i, dom)
    788       {
    789         const char *val;
    790 
    791         val = json_string_value (dom);
    792         GNUNET_break (NULL != val);
    793         if (NULL != val)
    794           new_family.details.subscription.trusted_domains[i]
    795             = GNUNET_strdup (val);
    796       }
    797       break;
    798     }
    799   case TALER_MERCHANTDB_TFK_Discount:
    800     {
    801       json_t *edomains = json_object_get (tf->extra_data,
    802                                           "expected_domains");
    803       json_t *dom;
    804       size_t i;
    805 
    806       new_family.kind = TALER_MERCHANT_CONTRACT_TOKEN_KIND_DISCOUNT;
    807       new_family.critical = false;
    808       new_family.details.discount.expected_domains_len
    809         = json_array_size (edomains);
    810       GNUNET_assert (new_family.details.discount.expected_domains_len
    811                      < UINT_MAX);
    812       new_family.details.discount.expected_domains
    813         = GNUNET_new_array (
    814             new_family.details.discount.expected_domains_len,
    815             char *);
    816       json_array_foreach (edomains, i, dom)
    817       {
    818         const char *val;
    819 
    820         val = json_string_value (dom);
    821         GNUNET_break (NULL != val);
    822         if (NULL != val)
    823           new_family.details.discount.expected_domains[i]
    824             = GNUNET_strdup (val);
    825       }
    826       break;
    827     }
    828   }
    829   *family = new_family;
    830 }
    831 
    832 
    833 /* end of taler-merchant-httpd_token-keys.c */