merchant

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

taler-merchant-httpd_mfa.c (23071B)


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