merchant

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

taler-merchant-httpd_mfa.c (22557B)


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