merchant

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

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


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