merchant

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

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


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