merchant

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

taler-merchant-httpd_mfa.c (23640B)


      1 /*
      2   This file is part of TALER
      3   (C) 2025 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify
      6   it under the terms of the GNU Affero General Public License as
      7   published by the Free Software Foundation; either version 3,
      8   or (at your option) any later version.
      9 
     10   TALER is distributed in the hope that it will be useful, but
     11   WITHOUT ANY WARRANTY; without even the implied warranty of
     12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13   GNU General Public License for more details.
     14 
     15   You should have received a copy of the GNU General Public
     16   License along with TALER; see the file COPYING.  If not,
     17   see <http://www.gnu.org/licenses/>
     18 */
     19 
     20 /**
     21  * @file src/backend/taler-merchant-httpd_mfa.c
     22  * @brief internal APIs for multi-factor authentication (MFA)
     23  * @author Christian Grothoff
     24  */
     25 #include "platform.h"
     26 #include "taler-merchant-httpd.h"
     27 #include "taler-merchant-httpd_mfa.h"
     28 #include "merchant-database/insert_mfa_challenge.h"
     29 #include "merchant-database/get_mfa_challenge.h"
     30 
     31 
     32 /**
     33  * How many challenges do we allow at most per request?
     34  */
     35 #define MAX_CHALLENGES 9
     36 
     37 /**
     38  * How long are challenges valid?
     39  */
     40 #define CHALLENGE_LIFETIME GNUNET_TIME_UNIT_DAYS
     41 
     42 
     43 enum GNUNET_GenericReturnValue
     44 TMH_mfa_parse_challenge_id (struct TMH_HandlerContext *hc,
     45                             const char *challenge_id,
     46                             uint64_t *challenge_serial,
     47                             struct TALER_MERCHANT_MFA_BodyHash *h_body)
     48 {
     49   const char *dash = strchr (challenge_id,
     50                              '-');
     51   unsigned long long ser;
     52   char min;
     53 
     54   if (NULL == dash)
     55   {
     56     GNUNET_break_op (0);
     57     return (MHD_NO ==
     58             TALER_MHD_reply_with_error (hc->connection,
     59                                         MHD_HTTP_BAD_REQUEST,
     60                                         TALER_EC_GENERIC_PARAMETER_MALFORMED,
     61                                         "'-' missing in challenge ID"))
     62       ? GNUNET_SYSERR
     63       : GNUNET_NO;
     64   }
     65   if ( (2 !=
     66         sscanf (challenge_id,
     67                 "%llu%c%*s",
     68                 &ser,
     69                 &min)) ||
     70        ('-' != min) )
     71   {
     72     GNUNET_break_op (0);
     73     return (MHD_NO ==
     74             TALER_MHD_reply_with_error (hc->connection,
     75                                         MHD_HTTP_BAD_REQUEST,
     76                                         TALER_EC_GENERIC_PARAMETER_MALFORMED,
     77                                         "Invalid number for challenge ID"))
     78       ? GNUNET_SYSERR
     79       : GNUNET_NO;
     80   }
     81   if (GNUNET_OK !=
     82       GNUNET_STRINGS_string_to_data (dash + 1,
     83                                      strlen (dash + 1),
     84                                      h_body,
     85                                      sizeof (*h_body)))
     86   {
     87     GNUNET_break_op (0);
     88     return (MHD_NO ==
     89             TALER_MHD_reply_with_error (hc->connection,
     90                                         MHD_HTTP_BAD_REQUEST,
     91                                         TALER_EC_GENERIC_PARAMETER_MALFORMED,
     92                                         "Malformed challenge ID"))
     93       ? GNUNET_SYSERR
     94       : GNUNET_NO;
     95   }
     96   *challenge_serial = (uint64_t) ser;
     97   return GNUNET_OK;
     98 }
     99 
    100 
    101 /**
    102  * Check if the given authentication check was already completed.
    103  *
    104  * @param[in,out] hc handler context of the connection to authorize
    105  * @param instance_name instance for which the challenge must have been issued
    106  * @param op operation for which we are requiring authorization
    107  * @param challenge_id ID of the challenge to check if it is done
    108  * @param[out] solved set to true if the challenge was solved,
    109  *             set to false if @a challenge_id was not found
    110  * @param[out] channel TAN channel that was used,
    111  *             set to #TALER_MERCHANT_MFA_CHANNEL_NONE if @a challenge_id
    112  *             was not found
    113  * @param[out] target_address address which was validated,
    114  *             set to NULL if @a challenge_id was not found
    115  * @param[out] retry_counter how many attempts are left on the challenge
    116  * @return #GNUNET_OK on success (challenge found)
    117  *         #GNUNET_NO if an error message was returned to the client
    118  *         #GNUNET_SYSERR to just close the connection
    119  */
    120 static enum GNUNET_GenericReturnValue
    121 mfa_challenge_check (
    122   struct TMH_HandlerContext *hc,
    123   const char *instance_name,
    124   enum TALER_MERCHANT_MFA_CriticalOperation op,
    125   const char *challenge_id,
    126   bool *solved,
    127   enum TALER_MERCHANT_MFA_Channel *channel,
    128   char **target_address,
    129   uint32_t *retry_counter)
    130 {
    131   uint64_t challenge_serial;
    132   struct TALER_MERCHANT_MFA_BodyHash h_body;
    133   struct TALER_MERCHANT_MFA_BodyHash x_h_body;
    134   struct TALER_MERCHANT_MFA_BodySalt salt;
    135   struct GNUNET_TIME_Absolute retransmission_date;
    136   enum TALER_MERCHANT_MFA_CriticalOperation xop;
    137   enum GNUNET_DB_QueryStatus qs;
    138   struct GNUNET_TIME_Absolute confirmation_date;
    139   enum GNUNET_GenericReturnValue ret;
    140   char *instance_id = NULL;
    141 
    142   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    143               "Checking status of challenge %s\n",
    144               challenge_id);
    145   ret = TMH_mfa_parse_challenge_id (hc,
    146                                     challenge_id,
    147                                     &challenge_serial,
    148                                     &x_h_body);
    149   if (GNUNET_OK != ret)
    150     return ret;
    151   *target_address = NULL;
    152   *solved = false;
    153   *channel = TALER_MERCHANT_MFA_CHANNEL_NONE;
    154   *retry_counter = UINT_MAX;
    155   qs = TALER_MERCHANTDB_get_mfa_challenge (TMH_db,
    156                                            challenge_serial,
    157                                            &x_h_body,
    158                                            &salt,
    159                                            target_address,
    160                                            &xop,
    161                                            &confirmation_date,
    162                                            &retransmission_date,
    163                                            retry_counter,
    164                                            channel,
    165                                            &instance_id);
    166   switch (qs)
    167   {
    168   case GNUNET_DB_STATUS_HARD_ERROR:
    169     GNUNET_break (0);
    170     return (MHD_NO ==
    171             TALER_MHD_reply_with_error (hc->connection,
    172                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    173                                         TALER_EC_GENERIC_DB_COMMIT_FAILED,
    174                                         NULL))
    175       ? GNUNET_SYSERR
    176       : GNUNET_NO;
    177   case GNUNET_DB_STATUS_SOFT_ERROR:
    178     GNUNET_break (0);
    179     return (MHD_NO ==
    180             TALER_MHD_reply_with_error (hc->connection,
    181                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    182                                         TALER_EC_GENERIC_DB_SOFT_FAILURE,
    183                                         NULL))
    184       ? GNUNET_SYSERR
    185       : GNUNET_NO;
    186   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    187     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    188                 "Challenge %s not found\n",
    189                 challenge_id);
    190     return GNUNET_OK;
    191   case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    192     break;
    193   }
    194   if (0 != strcmp (instance_name,
    195                    instance_id))
    196   {
    197     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    198                 "Challenge was issued for a different instance (%s!=%s)!\n",
    199                 instance_id,
    200                 instance_name);
    201     GNUNET_free (instance_id);
    202     GNUNET_free (*target_address);
    203     *target_address = NULL;
    204     *channel = TALER_MERCHANT_MFA_CHANNEL_NONE;
    205     *retry_counter = UINT_MAX;
    206     return GNUNET_OK;
    207   }
    208   GNUNET_free (instance_id);
    209 
    210   if (xop != op)
    211   {
    212     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    213                 "Challenge was for a different operation (%d!=%d)!\n",
    214                 (int) op,
    215                 (int) xop);
    216     *solved = false;
    217     return GNUNET_OK;
    218   }
    219   TALER_MERCHANT_mfa_body_hash (hc->request_body,
    220                                 &salt,
    221                                 &h_body);
    222   if (0 !=
    223       GNUNET_memcmp (&h_body,
    224                      &x_h_body))
    225   {
    226     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    227                 "Challenge was for a different request body!\n");
    228     *solved = false;
    229     return GNUNET_OK;
    230   }
    231   *solved = (! GNUNET_TIME_absolute_is_future (confirmation_date));
    232   return GNUNET_OK;
    233 }
    234 
    235 
    236 /**
    237  * Multi-factor authentication check to see if for the given @a instance_id
    238  * and the @a op operation all the TAN channels given in @a required_tans have
    239  * been satisfied.  Note that we always satisfy @a required_tans in the order
    240  * given in the array, so if the last one is satisfied, all previous ones must
    241  * have been satisfied before.
    242  *
    243  * If the challenges has not been satisfied, an appropriate response
    244  * is returned to the client of @a hc.
    245  *
    246  * @param[in,out] hc handler context of the connection to authorize
    247  * @param instance_name instance name to use in the message to the customer
    248  * @param op operation for which we are performing
    249  * @param channel TAN channel to try
    250  * @param expiration_date when should the challenge expire
    251  * @param required_address addresses to use for
    252  *        the respective challenge
    253  * @param[out] challenge_id set to the challenge ID, to be freed by
    254  *   the caller
    255  * @return #GNUNET_OK on success,
    256  *         #GNUNET_NO if an error message was returned to the client
    257  *         #GNUNET_SYSERR to just close the connection
    258  */
    259 static enum GNUNET_GenericReturnValue
    260 mfa_challenge_start (
    261   struct TMH_HandlerContext *hc,
    262   const char *instance_name,
    263   enum TALER_MERCHANT_MFA_CriticalOperation op,
    264   enum TALER_MERCHANT_MFA_Channel channel,
    265   struct GNUNET_TIME_Absolute expiration_date,
    266   const char *required_address,
    267   char **challenge_id)
    268 {
    269   enum GNUNET_DB_QueryStatus qs;
    270   struct TALER_MERCHANT_MFA_BodySalt salt;
    271   struct TALER_MERCHANT_MFA_BodyHash h_body;
    272   uint64_t challenge_serial;
    273   unsigned long long challenge_num;
    274   char *code;
    275 
    276   GNUNET_CRYPTO_random_block (&salt,
    277                               sizeof (salt));
    278   TALER_MERCHANT_mfa_body_hash (hc->request_body,
    279                                 &salt,
    280                                 &h_body);
    281   challenge_num = (unsigned long long)
    282                   GNUNET_CRYPTO_random_u64 (1000 * 1000 * 100);
    283   /* Note: if this is changed, the code in
    284      taler-merchant-httpd_post-challenge-ID.c and
    285      taler-merchant-httpd_post-challenge-ID-confirm.c must
    286      possibly also be updated! */
    287   GNUNET_asprintf (&code,
    288                    "%04llu-%04llu",
    289                    challenge_num / 10000,
    290                    challenge_num % 10000);
    291   qs = TALER_MERCHANTDB_insert_mfa_challenge (TMH_db,
    292                                               instance_name,
    293                                               op,
    294                                               &h_body,
    295                                               &salt,
    296                                               code,
    297                                               expiration_date,
    298                                               GNUNET_TIME_UNIT_ZERO_ABS,
    299                                               channel,
    300                                               required_address,
    301                                               &challenge_serial);
    302   GNUNET_free (code);
    303   switch (qs)
    304   {
    305   case GNUNET_DB_STATUS_HARD_ERROR:
    306     GNUNET_break (0);
    307     return (MHD_NO ==
    308             TALER_MHD_reply_with_error (hc->connection,
    309                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    310                                         TALER_EC_GENERIC_DB_COMMIT_FAILED,
    311                                         NULL))
    312       ? GNUNET_SYSERR
    313       : GNUNET_NO;
    314   case GNUNET_DB_STATUS_SOFT_ERROR:
    315     GNUNET_break (0);
    316     return (MHD_NO ==
    317             TALER_MHD_reply_with_error (hc->connection,
    318                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    319                                         TALER_EC_GENERIC_DB_SOFT_FAILURE,
    320                                         NULL))
    321       ? GNUNET_SYSERR
    322       : GNUNET_NO;
    323   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    324     GNUNET_assert (0);
    325     break;
    326   case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    327     break;
    328   }
    329   {
    330     char *h_body_s;
    331 
    332     h_body_s = GNUNET_STRINGS_data_to_string_alloc (&h_body,
    333                                                     sizeof (h_body));
    334     GNUNET_asprintf (challenge_id,
    335                      "%llu-%s",
    336                      (unsigned long long) challenge_serial,
    337                      h_body_s);
    338     GNUNET_free (h_body_s);
    339   }
    340   return GNUNET_OK;
    341 }
    342 
    343 
    344 /**
    345  * Internal book-keeping for #TMH_mfa_challenges_do().
    346  */
    347 struct Challenge
    348 {
    349   /**
    350    * Channel on which the challenge is transmitted.
    351    */
    352   enum TALER_MERCHANT_MFA_Channel channel;
    353 
    354   /**
    355    * Address to send the challenge to.
    356    */
    357   const char *required_address;
    358 
    359   /**
    360    * Internal challenge ID.
    361    */
    362   char *challenge_id;
    363 
    364   /**
    365    * True if the challenge was solved.
    366    */
    367   bool solved;
    368 
    369   /**
    370    * True if the challenge could still be solved.
    371    */
    372   bool solvable;
    373 
    374 };
    375 
    376 
    377 /**
    378  * Obtain hint about the @a target_address of type @a channel to
    379  * return to the client.
    380  *
    381  * @param channel type of challenge
    382  * @param target_address address we will sent the challenge to
    383  * @return hint for the user about the address
    384  */
    385 static char *
    386 get_hint (enum TALER_MERCHANT_MFA_Channel channel,
    387           const char *target_address)
    388 {
    389   switch (channel)
    390   {
    391   case TALER_MERCHANT_MFA_CHANNEL_NONE:
    392     GNUNET_assert (0);
    393     return NULL;
    394   case TALER_MERCHANT_MFA_CHANNEL_SMS:
    395     {
    396       size_t slen = strlen (target_address);
    397       const char *end;
    398 
    399       if (slen > 4)
    400         end = &target_address[slen - 4];
    401       else
    402         end = &target_address[slen / 2];
    403       return GNUNET_strdup (end);
    404     }
    405   case TALER_MERCHANT_MFA_CHANNEL_EMAIL:
    406     {
    407       const char *at;
    408       size_t len;
    409 
    410       at = strchr (target_address,
    411                    '@');
    412       if (NULL == at)
    413         len = 0;
    414       else
    415         len = at - target_address;
    416       return GNUNET_strndup (target_address,
    417                              len);
    418     }
    419   case TALER_MERCHANT_MFA_CHANNEL_TOTP:
    420     GNUNET_break (0);
    421     return GNUNET_strdup ("TOTP is not implemented: #10327");
    422   }
    423   GNUNET_break (0);
    424   return NULL;
    425 }
    426 
    427 
    428 /**
    429  * Check that a set of MFA challenges has been satisfied by the
    430  * client for the request in @a hc.
    431  *
    432  * @param[in,out] hc handler context with the connection to the client
    433  * @param instance_name instance name to use in the message to the customer
    434  * @param op operation for which we should check challenges for
    435  * @param combi_and true to tell the client to solve all challenges (AND),
    436  *       false means that any of the challenges will do (OR)
    437  * @param ... pairs of channel and address, terminated by
    438  *        #TALER_MERCHANT_MFA_CHANNEL_NONE
    439  * @return #GNUNET_OK on success (challenges satisfied)
    440  *         #GNUNET_NO if an error message was returned to the client
    441  *         #GNUNET_SYSERR to just close the connection
    442  */
    443 enum GNUNET_GenericReturnValue
    444 TMH_mfa_challenges_do (
    445   struct TMH_HandlerContext *hc,
    446   const char *instance_name,
    447   enum TALER_MERCHANT_MFA_CriticalOperation op,
    448   bool combi_and,
    449   ...)
    450 {
    451   struct Challenge challenges[MAX_CHALLENGES];
    452   const char *challenge_ids[MAX_CHALLENGES];
    453   size_t num_challenges;
    454   char *challenge_ids_copy = NULL;
    455   size_t num_provided_challenges;
    456   enum GNUNET_GenericReturnValue ret;
    457 
    458   {
    459     va_list ap;
    460 
    461     va_start (ap,
    462               combi_and);
    463     num_challenges = 0;
    464     while (num_challenges < MAX_CHALLENGES)
    465     {
    466       enum TALER_MERCHANT_MFA_Channel channel;
    467       const char *address;
    468 
    469       channel = va_arg (ap,
    470                         enum TALER_MERCHANT_MFA_Channel);
    471       if (TALER_MERCHANT_MFA_CHANNEL_NONE == channel)
    472         break;
    473       address = va_arg (ap,
    474                         const char *);
    475       if (NULL == address)
    476         continue;
    477       challenges[num_challenges].channel = channel;
    478       challenges[num_challenges].required_address = address;
    479       challenges[num_challenges].challenge_id = NULL;
    480       challenges[num_challenges].solved = false;
    481       challenges[num_challenges].solvable = true;
    482       num_challenges++;
    483     }
    484     va_end (ap);
    485   }
    486 
    487   if (0 == num_challenges)
    488   {
    489     /* No challenges required. Strange... */
    490     return GNUNET_OK;
    491   }
    492 
    493   {
    494     const char *challenge_ids_header;
    495 
    496     challenge_ids_header
    497       = MHD_lookup_connection_value (hc->connection,
    498                                      MHD_HEADER_KIND,
    499                                      "Taler-Challenge-Ids");
    500     num_provided_challenges = 0;
    501     if (NULL != challenge_ids_header)
    502     {
    503       challenge_ids_copy = GNUNET_strdup (challenge_ids_header);
    504 
    505       for (char *token = strtok (challenge_ids_copy,
    506                                  ",");
    507            NULL != token;
    508            token = strtok (NULL,
    509                            ","))
    510       {
    511         if (num_provided_challenges >= MAX_CHALLENGES)
    512         {
    513           GNUNET_break_op (0);
    514           GNUNET_free (challenge_ids_copy);
    515           return (MHD_NO ==
    516                   TALER_MHD_reply_with_error (
    517                     hc->connection,
    518                     MHD_HTTP_BAD_REQUEST,
    519                     TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED,
    520                     "Taler-Challenge-Ids"))
    521           ? GNUNET_SYSERR
    522           : GNUNET_NO;
    523         }
    524         challenge_ids[num_provided_challenges] = token;
    525         num_provided_challenges++;
    526       }
    527     }
    528   }
    529 
    530   /* Check provided challenges against requirements */
    531   for (size_t i = 0; i < num_provided_challenges; i++)
    532   {
    533     bool solved;
    534     enum TALER_MERCHANT_MFA_Channel channel;
    535     char *target_address;
    536     uint32_t retry_counter;
    537 
    538     ret = mfa_challenge_check (hc,
    539                                instance_name,
    540                                op,
    541                                challenge_ids[i],
    542                                &solved,
    543                                &channel,
    544                                &target_address,
    545                                &retry_counter);
    546     if (GNUNET_OK != ret)
    547       goto cleanup;
    548     for (size_t j = 0; j < num_challenges; j++)
    549     {
    550       if ( (challenges[j].channel == channel) &&
    551            (NULL == challenges[j].challenge_id) &&
    552            (NULL != target_address /* just to be sure */) &&
    553            (0 == strcmp (target_address,
    554                          challenges[j].required_address) ) )
    555       {
    556         challenges[j].solved
    557           = solved;
    558         challenges[j].challenge_id
    559           = GNUNET_strdup (challenge_ids[i]);
    560         if ( (! solved) &&
    561              (0 == retry_counter) )
    562         {
    563           /* can't be solved anymore! */
    564           challenges[j].solvable = false;
    565         }
    566         break;
    567       }
    568     }
    569     GNUNET_free (target_address);
    570   }
    571 
    572   {
    573     struct GNUNET_TIME_Absolute expiration_date
    574       = GNUNET_TIME_relative_to_absolute (CHALLENGE_LIFETIME);
    575 
    576     /* Start new challenges for unsolved requirements */
    577     for (size_t i = 0; i < num_challenges; i++)
    578     {
    579       if (NULL == challenges[i].challenge_id)
    580       {
    581         GNUNET_assert (! challenges[i].solved);
    582         GNUNET_assert (challenges[i].solvable);
    583         ret = mfa_challenge_start (hc,
    584                                    instance_name,
    585                                    op,
    586                                    challenges[i].channel,
    587                                    expiration_date,
    588                                    challenges[i].required_address,
    589                                    &challenges[i].challenge_id);
    590         if (GNUNET_OK != ret)
    591           goto cleanup;
    592       }
    593     }
    594   }
    595 
    596   {
    597     bool all_solved = true;
    598     bool any_solved = false;
    599     bool solvable = true;
    600 
    601     for (size_t i = 0; i < num_challenges; i++)
    602     {
    603       if (challenges[i].solved)
    604       {
    605         any_solved = true;
    606       }
    607       else
    608       {
    609         all_solved = false;
    610         if (combi_and &&
    611             (! challenges[i].solvable) )
    612           solvable = false;
    613       }
    614     }
    615 
    616     if ( (combi_and && all_solved) ||
    617          (! combi_and && any_solved) )
    618     {
    619       /* Authorization successful */
    620       ret = GNUNET_OK;
    621       goto cleanup;
    622     }
    623     if (! solvable)
    624     {
    625       ret = (MHD_NO ==
    626              TALER_MHD_reply_with_error (
    627                hc->connection,
    628                MHD_HTTP_FORBIDDEN,
    629                TALER_EC_MERCHANT_MFA_FORBIDDEN,
    630                GNUNET_TIME_relative2s (CHALLENGE_LIFETIME,
    631                                        false)))
    632         ? GNUNET_SYSERR
    633         : GNUNET_NO;
    634       goto cleanup;
    635     }
    636   }
    637 
    638   /* Return challenges to client */
    639   {
    640     json_t *jchallenges;
    641 
    642     jchallenges = json_array ();
    643     GNUNET_assert (NULL != jchallenges);
    644     for (size_t i = 0; i<num_challenges; i++)
    645     {
    646       const struct Challenge *c = &challenges[i];
    647       json_t *jc;
    648       char *hint;
    649 
    650       hint = get_hint (c->channel,
    651                        c->required_address);
    652 
    653       jc = GNUNET_JSON_PACK (
    654         GNUNET_JSON_pack_string ("tan_info",
    655                                  hint),
    656         GNUNET_JSON_pack_string ("tan_channel",
    657                                  TALER_MERCHANT_MFA_channel_to_string (
    658                                    c->channel)),
    659         GNUNET_JSON_pack_string ("challenge_id",
    660                                  c->challenge_id));
    661       GNUNET_free (hint);
    662       GNUNET_assert (0 ==
    663                      json_array_append_new (
    664                        jchallenges,
    665                        jc));
    666     }
    667     ret = (MHD_NO ==
    668            TALER_MHD_REPLY_JSON_PACK (
    669              hc->connection,
    670              MHD_HTTP_ACCEPTED,
    671              GNUNET_JSON_pack_bool ("combi_and",
    672                                     combi_and),
    673              GNUNET_JSON_pack_array_steal ("challenges",
    674                                            jchallenges)))
    675       ? GNUNET_SYSERR
    676       : GNUNET_NO;
    677   }
    678 
    679 cleanup:
    680   for (size_t i = 0; i < num_challenges; i++)
    681     GNUNET_free (challenges[i].challenge_id);
    682   GNUNET_free (challenge_ids_copy);
    683   return ret;
    684 }
    685 
    686 
    687 enum GNUNET_GenericReturnValue
    688 TMH_mfa_check_simple (
    689   struct TMH_HandlerContext *hc,
    690   enum TALER_MERCHANT_MFA_CriticalOperation op,
    691   struct TMH_MerchantInstance *mi)
    692 {
    693   enum GNUNET_GenericReturnValue ret;
    694   bool have_sms = (NULL != mi->settings.phone) &&
    695                   (NULL != TMH_helper_sms) &&
    696                   (mi->settings.phone_validated);
    697   bool have_email = (NULL != mi->settings.email) &&
    698                     (NULL != TMH_helper_email) &&
    699                     (mi->settings.email_validated);
    700 
    701   /* Note: we check for 'validated' above, but in theory
    702      we could also use unvalidated for this operation.
    703      That's a policy-decision we may want to revise,
    704      but probably need to look at the global threat model to
    705      make sure alternative configurations are still sane. */
    706   if (have_email)
    707   {
    708     ret = TMH_mfa_challenges_do (hc,
    709                                  mi->settings.id,
    710                                  op,
    711                                  false,
    712                                  TALER_MERCHANT_MFA_CHANNEL_EMAIL,
    713                                  mi->settings.email,
    714                                  have_sms
    715                                  ? TALER_MERCHANT_MFA_CHANNEL_SMS
    716                                  : TALER_MERCHANT_MFA_CHANNEL_NONE,
    717                                  mi->settings.phone,
    718                                  TALER_MERCHANT_MFA_CHANNEL_NONE);
    719   }
    720   else if (have_sms)
    721   {
    722     ret = TMH_mfa_challenges_do (hc,
    723                                  mi->settings.id,
    724                                  op,
    725                                  false,
    726                                  TALER_MERCHANT_MFA_CHANNEL_SMS,
    727                                  mi->settings.phone,
    728                                  TALER_MERCHANT_MFA_CHANNEL_NONE);
    729   }
    730   else
    731   {
    732     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    733                 "No MFA possible, skipping 2-FA\n");
    734     ret = GNUNET_OK;
    735   }
    736   return ret;
    737 }