merchant

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

taler-merchant-httpd_post-challenge-ID.c (18481B)


      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_post-challenge-ID.c
     22  * @brief endpoint to trigger sending MFA challenge
     23  * @author Christian Grothoff
     24  */
     25 #include "taler/platform.h"
     26 #include "taler-merchant-httpd.h"
     27 #include "taler-merchant-httpd_mfa.h"
     28 #include "taler-merchant-httpd_post-challenge-ID.h"
     29 
     30 
     31 /**
     32  * How many attempts do we allow per solution at most? Note that
     33  * this is just for the API, the value must also match the
     34  * database logic in create_mfa_challenge.
     35  */
     36 #define MAX_SOLUTIONS 3
     37 
     38 
     39 /**
     40  * How long is an OTP code valid?
     41  */
     42 #define OTP_TIMEOUT GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 30)
     43 
     44 
     45 /**
     46  * Internal state for MFA processing.
     47  */
     48 struct MfaState
     49 {
     50 
     51   /**
     52    * Kept in a DLL.
     53    */
     54   struct MfaState *next;
     55 
     56   /**
     57    * Kept in a DLL.
     58    */
     59   struct MfaState *prev;
     60 
     61   /**
     62    * HTTP request we are handling.
     63    */
     64   struct TMH_HandlerContext *hc;
     65 
     66   /**
     67    * Challenge code.
     68    */
     69   char *code;
     70 
     71   /**
     72    * When does @e code expire?
     73    */
     74   struct GNUNET_TIME_Absolute expiration_date;
     75 
     76   /**
     77    * When may we transmit a new code?
     78    */
     79   struct GNUNET_TIME_Absolute retransmission_date;
     80 
     81   /**
     82    * Handle to the helper process.
     83    */
     84   struct GNUNET_Process *child;
     85 
     86   /**
     87    * Handle to wait for @e child
     88    */
     89   struct GNUNET_ChildWaitHandle *cwh;
     90 
     91   /**
     92    * Address where to send the challenge.
     93    */
     94   char *required_address;
     95 
     96   /**
     97    * Message to send.
     98    */
     99   char *msg;
    100 
    101   /**
    102    * Instance the challenge is for.
    103    */
    104   char *instance_id;
    105 
    106   /**
    107    * Offset of transmission in msg.
    108    */
    109   size_t msg_off;
    110 
    111   /**
    112    * ID of our challenge.
    113    */
    114   uint64_t challenge_id;
    115 
    116   /**
    117    * Salted hash over the request body.
    118    */
    119   struct TALER_MERCHANT_MFA_BodyHash h_body;
    120 
    121   /**
    122    * Channel to use for the challenge.
    123    */
    124   enum TALER_MERCHANT_MFA_Channel channel;
    125 
    126   enum
    127   {
    128     MFA_PHASE_PARSE = 0,
    129     MFA_PHASE_LOOKUP,
    130     MFA_PHASE_SENDING,
    131     MFA_PHASE_SUSPENDING,
    132     MFA_PHASE_SENT,
    133     MFA_PHASE_RETURN_YES,
    134     MFA_PHASE_RETURN_NO,
    135 
    136   } phase;
    137 
    138 
    139   /**
    140    * #GNUNET_NO if the @e connection was not suspended,
    141    * #GNUNET_YES if the @e connection was suspended,
    142    * #GNUNET_SYSERR if @e connection was resumed to as
    143    * part of #THM_mfa_done during shutdown.
    144    */
    145   enum GNUNET_GenericReturnValue suspended;
    146 
    147   /**
    148    * Type of critical operation being authorized.
    149    */
    150   enum TALER_MERCHANT_MFA_CriticalOperation op;
    151 
    152   /**
    153    * Set to true if sending worked.
    154    */
    155   bool send_ok;
    156 };
    157 
    158 
    159 /**
    160  * Kept in a DLL.
    161  */
    162 static struct MfaState *mfa_head;
    163 
    164 /**
    165  * Kept in a DLL.
    166  */
    167 static struct MfaState *mfa_tail;
    168 
    169 
    170 /**
    171  * Clean up @a mfa process.
    172  *
    173  * @param[in] cls the `struct MfaState` to clean up
    174  */
    175 static void
    176 mfa_context_cleanup (void *cls)
    177 {
    178   struct MfaState *mfa = cls;
    179 
    180   GNUNET_CONTAINER_DLL_remove (mfa_head,
    181                                mfa_tail,
    182                                mfa);
    183   if (NULL != mfa->cwh)
    184   {
    185     GNUNET_wait_child_cancel (mfa->cwh);
    186     mfa->cwh = NULL;
    187   }
    188   if (NULL != mfa->child)
    189   {
    190     GNUNET_break (GNUNET_OK ==
    191                   GNUNET_process_kill (mfa->child,
    192                                        SIGKILL));
    193     GNUNET_break (GNUNET_OK ==
    194                   GNUNET_process_wait (mfa->child,
    195                                        true,
    196                                        NULL,
    197                                        NULL));
    198     GNUNET_process_destroy (mfa->child);
    199     mfa->child = NULL;
    200   }
    201   GNUNET_free (mfa->required_address);
    202   GNUNET_free (mfa->msg);
    203   GNUNET_free (mfa->instance_id);
    204   GNUNET_free (mfa->code);
    205   GNUNET_free (mfa);
    206 }
    207 
    208 
    209 void
    210 TMH_challenge_done ()
    211 {
    212   for (struct MfaState *mfa = mfa_head;
    213        NULL != mfa;
    214        mfa = mfa->next)
    215   {
    216     if (GNUNET_YES == mfa->suspended)
    217     {
    218       mfa->suspended = GNUNET_SYSERR;
    219       MHD_resume_connection (mfa->hc->connection);
    220     }
    221   }
    222 }
    223 
    224 
    225 /**
    226  * Send the given @a response for the @a mfa request.
    227  *
    228  * @param[in,out] mfa process to generate an error response for
    229  * @param response_code response code to use
    230  * @param[in] response response data to send back
    231  */
    232 static void
    233 respond_to_challenge_with_response (struct MfaState *mfa,
    234                                     unsigned int response_code,
    235                                     struct MHD_Response *response)
    236 {
    237   MHD_RESULT res;
    238 
    239   res = MHD_queue_response (mfa->hc->connection,
    240                             response_code,
    241                             response);
    242   MHD_destroy_response (response);
    243   mfa->phase = (MHD_NO == res)
    244     ? MFA_PHASE_RETURN_NO
    245     : MFA_PHASE_RETURN_YES;
    246 }
    247 
    248 
    249 /**
    250  * Generate an error for @a mfa.
    251  *
    252  * @param[in,out] mfa process to generate an error response for
    253  * @param http_status HTTP status of the response
    254  * @param ec Taler error code to return
    255  * @param hint hint to return, can be NULL
    256  */
    257 static void
    258 respond_with_error (struct MfaState *mfa,
    259                     unsigned int http_status,
    260                     enum TALER_ErrorCode ec,
    261                     const char *hint)
    262 {
    263   respond_to_challenge_with_response (
    264     mfa,
    265     http_status,
    266     TALER_MHD_make_error (ec,
    267                           hint));
    268 }
    269 
    270 
    271 /**
    272  * Challenge code transmission complete. Continue based on the result.
    273  *
    274  * @param[in,out] mfa process to send the challenge for
    275  */
    276 static void
    277 phase_sent (struct MfaState *mfa)
    278 {
    279   enum GNUNET_DB_QueryStatus qs;
    280 
    281   if (! mfa->send_ok)
    282   {
    283     respond_with_error (mfa,
    284                         MHD_HTTP_BAD_GATEWAY,
    285                         TALER_EC_MERCHANT_TAN_MFA_HELPER_EXEC_FAILED,
    286                         "process exited with error");
    287     return;
    288   }
    289   qs = TMH_db->update_mfa_challenge (TMH_db->cls,
    290                                      mfa->challenge_id,
    291                                      mfa->code,
    292                                      MAX_SOLUTIONS,
    293                                      mfa->expiration_date,
    294                                      mfa->retransmission_date);
    295   switch (qs)
    296   {
    297   case GNUNET_DB_STATUS_HARD_ERROR:
    298     GNUNET_break (0);
    299     respond_with_error (mfa,
    300                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    301                         TALER_EC_GENERIC_DB_COMMIT_FAILED,
    302                         "update_mfa_challenge");
    303     return;
    304   case GNUNET_DB_STATUS_SOFT_ERROR:
    305     GNUNET_break (0);
    306     respond_with_error (mfa,
    307                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    308                         TALER_EC_GENERIC_DB_SOFT_FAILURE,
    309                         "update_mfa_challenge");
    310     return;
    311   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    312     GNUNET_break (0);
    313     respond_with_error (mfa,
    314                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    315                         TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
    316                         "no results on INSERT, but success?");
    317     return;
    318   case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    319     break;
    320   }
    321   {
    322     struct MHD_Response *response;
    323 
    324     response =
    325       TALER_MHD_make_json_steal (
    326         GNUNET_JSON_PACK (
    327           GNUNET_JSON_pack_timestamp (
    328             "solve_expiration",
    329             GNUNET_TIME_absolute_to_timestamp (
    330               mfa->expiration_date)),
    331           GNUNET_JSON_pack_timestamp (
    332             "earliest_retransmission",
    333             GNUNET_TIME_absolute_to_timestamp (
    334               mfa->retransmission_date))));
    335     respond_to_challenge_with_response (
    336       mfa,
    337       MHD_HTTP_OK,
    338       response);
    339   }
    340 }
    341 
    342 
    343 /**
    344  * Function called when our SMS helper has terminated.
    345  *
    346  * @param cls our `struct ANASTASIS_AUHTORIZATION_State`
    347  * @param type type of the process
    348  * @param exit_code status code of the process
    349  */
    350 static void
    351 transmission_done_cb (void *cls,
    352                       enum GNUNET_OS_ProcessStatusType type,
    353                       long unsigned int exit_code)
    354 {
    355   struct MfaState *mfa = cls;
    356 
    357   mfa->cwh = NULL;
    358   if (NULL != mfa->child)
    359   {
    360     GNUNET_process_destroy (mfa->child);
    361     mfa->child = NULL;
    362   }
    363   mfa->send_ok = ( (GNUNET_OS_PROCESS_EXITED == type) &&
    364                    (0 == exit_code) );
    365   if (! mfa->send_ok)
    366     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    367                 "MFA helper failed with status %d/%u\n",
    368                 (int) type,
    369                 (unsigned int) exit_code);
    370   mfa->phase = MFA_PHASE_SENT;
    371   GNUNET_assert (GNUNET_YES == mfa->suspended);
    372   mfa->suspended = GNUNET_NO;
    373   MHD_resume_connection (mfa->hc->connection);
    374   TALER_MHD_daemon_trigger ();
    375 }
    376 
    377 
    378 /**
    379  * Setup challenge code for @a mfa and send it to the
    380  * @a required_address; on success.
    381  *
    382  * @param[in,out] mfa process to send the challenge for
    383  * @param required_address where to send the challenge
    384  */
    385 static void
    386 phase_send_challenge (struct MfaState *mfa)
    387 {
    388   const char *prog = NULL;
    389   unsigned long long challenge_num;
    390 
    391   challenge_num = (unsigned long long)
    392                   GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_NONCE,
    393                                             1000 * 1000 * 100);
    394   GNUNET_asprintf (&mfa->code,
    395                    "%04llu-%04llu",
    396                    challenge_num / 10000,
    397                    challenge_num % 10000);
    398   switch (mfa->channel)
    399   {
    400   case TALER_MERCHANT_MFA_CHANNEL_NONE:
    401     GNUNET_assert (0);
    402     break;
    403   case TALER_MERCHANT_MFA_CHANNEL_SMS:
    404     mfa->expiration_date
    405       = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS);
    406     mfa->retransmission_date
    407       = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS);
    408     prog = TMH_helper_sms;
    409     break;
    410   case TALER_MERCHANT_MFA_CHANNEL_EMAIL:
    411     mfa->expiration_date
    412       = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS);
    413     mfa->retransmission_date
    414       = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS);
    415     prog = TMH_helper_email;
    416     break;
    417   case TALER_MERCHANT_MFA_CHANNEL_TOTP:
    418     mfa->expiration_date
    419       = GNUNET_TIME_relative_to_absolute (OTP_TIMEOUT);
    420     mfa->retransmission_date
    421       = GNUNET_TIME_relative_to_absolute (OTP_TIMEOUT);
    422     respond_with_error (mfa,
    423                         MHD_HTTP_NOT_IMPLEMENTED,
    424                         TALER_EC_GENERIC_FEATURE_NOT_IMPLEMENTED,
    425                         "#10327");
    426     return;
    427   }
    428   if (NULL == prog)
    429   {
    430     respond_with_error (
    431       mfa,
    432       MHD_HTTP_INTERNAL_SERVER_ERROR,
    433       TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
    434       TALER_MERCHANT_MFA_channel_to_string (mfa->channel));
    435     return;
    436   }
    437   {
    438     /* Start child process and feed pipe */
    439     struct GNUNET_DISK_PipeHandle *p;
    440     struct GNUNET_DISK_FileHandle *pipe_stdin;
    441 
    442     p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW);
    443     if (NULL == p)
    444     {
    445       respond_with_error (mfa,
    446                           MHD_HTTP_INTERNAL_SERVER_ERROR,
    447                           TALER_EC_GENERIC_ALLOCATION_FAILURE,
    448                           "pipe");
    449       return;
    450     }
    451     mfa->child = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR);
    452     GNUNET_assert (GNUNET_OK ==
    453                    GNUNET_process_set_options (
    454                      mfa->child,
    455                      GNUNET_process_option_inherit_rpipe (p,
    456                                                           STDIN_FILENO)));
    457     if (GNUNET_OK !=
    458         GNUNET_process_run_command_va (mfa->child,
    459                                        prog,
    460                                        prog,
    461                                        mfa->required_address,
    462                                        NULL))
    463     {
    464       GNUNET_process_destroy (mfa->child);
    465       mfa->child = NULL;
    466       GNUNET_break (GNUNET_OK ==
    467                     GNUNET_DISK_pipe_close (p));
    468       respond_with_error (mfa,
    469                           MHD_HTTP_BAD_GATEWAY,
    470                           TALER_EC_MERCHANT_TAN_MFA_HELPER_EXEC_FAILED,
    471                           "exec");
    472       return;
    473     }
    474 
    475     pipe_stdin = GNUNET_DISK_pipe_detach_end (p,
    476                                               GNUNET_DISK_PIPE_END_WRITE);
    477     GNUNET_assert (NULL != pipe_stdin);
    478     GNUNET_break (GNUNET_OK ==
    479                   GNUNET_DISK_pipe_close (p));
    480     GNUNET_asprintf (&mfa->msg,
    481                      "%s is your security code.\n"
    482                      "Do not share your code with anyone.\n\n"
    483                      "Authorizes: %s\n"
    484                      "Login: %s\n\n"
    485                      "Expires: %s (%s).\n",
    486                      mfa->code,
    487                      TALER_MERCHANT_MFA_co2s (mfa->op),
    488                      mfa->instance_id,
    489                      GNUNET_TIME_absolute2s (
    490                        mfa->expiration_date),
    491                      GNUNET_TIME_relative2s (
    492                        GNUNET_TIME_absolute_get_remaining (
    493                          mfa->expiration_date),
    494                        true));
    495     {
    496       const char *off = mfa->msg;
    497       size_t left = strlen (off);
    498 
    499       while (0 != left)
    500       {
    501         ssize_t ret;
    502 
    503         ret = GNUNET_DISK_file_write (pipe_stdin,
    504                                       off,
    505                                       left);
    506         if (ret <= 0)
    507         {
    508           respond_with_error (mfa,
    509                               MHD_HTTP_BAD_GATEWAY,
    510                               TALER_EC_MERCHANT_TAN_MFA_HELPER_EXEC_FAILED,
    511                               "write");
    512           return;
    513         }
    514         mfa->msg_off += ret;
    515         off += ret;
    516         left -= ret;
    517       }
    518       GNUNET_DISK_file_close (pipe_stdin);
    519     }
    520   }
    521   mfa->phase = MFA_PHASE_SUSPENDING;
    522 }
    523 
    524 
    525 /**
    526  * Lookup challenge in DB.
    527  *
    528  * @param[in,out] mfa process to parse data for
    529  */
    530 static void
    531 phase_lookup (struct MfaState *mfa)
    532 {
    533   enum GNUNET_DB_QueryStatus qs;
    534   uint32_t retry_counter;
    535   struct GNUNET_TIME_Absolute confirmation_date;
    536   struct GNUNET_TIME_Absolute retransmission_date;
    537   struct TALER_MERCHANT_MFA_BodySalt salt;
    538 
    539   qs = TMH_db->lookup_mfa_challenge (TMH_db->cls,
    540                                      mfa->challenge_id,
    541                                      &mfa->h_body,
    542                                      &salt,
    543                                      &mfa->required_address,
    544                                      &mfa->op,
    545                                      &confirmation_date,
    546                                      &retransmission_date,
    547                                      &retry_counter,
    548                                      &mfa->channel,
    549                                      &mfa->instance_id);
    550   switch (qs)
    551   {
    552   case GNUNET_DB_STATUS_HARD_ERROR:
    553     GNUNET_break (0);
    554     respond_with_error (mfa,
    555                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    556                         TALER_EC_GENERIC_DB_COMMIT_FAILED,
    557                         "lookup_mfa_challenge");
    558     return;
    559   case GNUNET_DB_STATUS_SOFT_ERROR:
    560     GNUNET_break (0);
    561     respond_with_error (mfa,
    562                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    563                         TALER_EC_GENERIC_DB_SOFT_FAILURE,
    564                         "lookup_mfa_challenge");
    565     return;
    566   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    567     GNUNET_break_op (0);
    568     respond_with_error (mfa,
    569                         MHD_HTTP_NOT_FOUND,
    570                         TALER_EC_MERCHANT_TAN_CHALLENGE_UNKNOWN,
    571                         mfa->hc->infix);
    572     return;
    573   case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    574     break;
    575   }
    576   if (! GNUNET_TIME_absolute_is_future (confirmation_date))
    577   {
    578     /* was already solved */
    579     respond_with_error (mfa,
    580                         MHD_HTTP_GONE,
    581                         TALER_EC_MERCHANT_TAN_CHALLENGE_SOLVED,
    582                         NULL);
    583     return;
    584   }
    585   if (GNUNET_TIME_absolute_is_future (retransmission_date))
    586   {
    587     /* too early to try again */
    588     respond_with_error (mfa,
    589                         MHD_HTTP_TOO_MANY_REQUESTS,
    590                         TALER_EC_MERCHANT_TAN_TOO_EARLY,
    591                         GNUNET_TIME_absolute2s (retransmission_date));
    592     return;
    593   }
    594   mfa->phase++;
    595 }
    596 
    597 
    598 /**
    599  * Parse challenge request.
    600  *
    601  * @param[in,out] mfa process to parse data for
    602  */
    603 static void
    604 phase_parse (struct MfaState *mfa)
    605 {
    606   struct TMH_HandlerContext *hc = mfa->hc;
    607   enum GNUNET_GenericReturnValue ret;
    608 
    609   ret = TMH_mfa_parse_challenge_id (hc,
    610                                     hc->infix,
    611                                     &mfa->challenge_id,
    612                                     &mfa->h_body);
    613   if (GNUNET_OK != ret)
    614   {
    615     mfa->phase = (GNUNET_NO == ret)
    616       ? MFA_PHASE_RETURN_YES
    617       : MFA_PHASE_RETURN_NO;
    618     return;
    619   }
    620   mfa->phase++;
    621 }
    622 
    623 
    624 MHD_RESULT
    625 TMH_post_challenge_ID (const struct TMH_RequestHandler *rh,
    626                        struct MHD_Connection *connection,
    627                        struct TMH_HandlerContext *hc)
    628 {
    629   struct MfaState *mfa = hc->ctx;
    630 
    631   if (NULL == mfa)
    632   {
    633     mfa = GNUNET_new (struct MfaState);
    634     mfa->hc = hc;
    635     hc->ctx = mfa;
    636     hc->cc = &mfa_context_cleanup;
    637     GNUNET_CONTAINER_DLL_insert (mfa_head,
    638                                  mfa_tail,
    639                                  mfa);
    640   }
    641 
    642   while (1)
    643   {
    644     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    645                 "Processing /challenge in phase %d\n",
    646                 (int) mfa->phase);
    647     switch (mfa->phase)
    648     {
    649     case MFA_PHASE_PARSE:
    650       phase_parse (mfa);
    651       break;
    652     case MFA_PHASE_LOOKUP:
    653       phase_lookup (mfa);
    654       break;
    655     case MFA_PHASE_SENDING:
    656       phase_send_challenge (mfa);
    657       break;
    658     case MFA_PHASE_SUSPENDING:
    659       mfa->cwh = GNUNET_wait_child (mfa->child,
    660                                     &transmission_done_cb,
    661                                     mfa);
    662       if (NULL == mfa->cwh)
    663       {
    664         respond_with_error (mfa,
    665                             MHD_HTTP_INTERNAL_SERVER_ERROR,
    666                             TALER_EC_GENERIC_ALLOCATION_FAILURE,
    667                             "GNUNET_wait_child");
    668         continue;
    669       }
    670       mfa->suspended = GNUNET_YES;
    671       MHD_suspend_connection (hc->connection);
    672       return MHD_YES;
    673     case MFA_PHASE_SENT:
    674       phase_sent (mfa);
    675       break;
    676     case MFA_PHASE_RETURN_YES:
    677       return MHD_YES;
    678     case MFA_PHASE_RETURN_NO:
    679       GNUNET_break (0);
    680       return MHD_NO;
    681     }
    682   }
    683 }