frosix

Multiparty signature service (experimental)
Log | Files | Refs | README | LICENSE

frosix-httpd_sig-commitment.c (17979B)


      1 /*
      2   This file is part of Frosix
      3   Copyright (C) 2022, 2023 Joel Urech
      4 
      5   Frosix is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   Frosix is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   Frosix; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file backend/frosix-httpd_sign-commitment.c
     18  * @brief functions to handle incoming requests on /sign-commitment
     19  * @author Joel Urech
     20  */
     21 #include "frosix-httpd_sig.h"
     22 #include "frosix-httpd.h"
     23 #include "frosix_database_plugin.h"
     24 #include "frost_high.h"
     25 #include "frost_low.h"
     26 #include <taler/taler_util.h>
     27 #include <gnunet/gnunet_util_lib.h>
     28 #include <gnunet/gnunet_db_lib.h>
     29 #include "frosix_authorization_plugin.h"
     30 
     31 /**
     32  * What is the maximum frequency at which we allow
     33  * clients to attempt to answer security questions?
     34  */
     35 #define MAX_QUESTION_FREQ GNUNET_TIME_relative_multiply ( \
     36     GNUNET_TIME_UNIT_SECONDS, 30)
     37 
     38 /**
     39  * How many retries do we allow per code?
     40  */
     41 #define INITIAL_RETRY_COUNTER 3
     42 
     43 struct SignCommitmentContext
     44 {
     45   /**
     46    *
     47   */
     48   uint32_t identifier;
     49 
     50   /**
     51    *
     52   */
     53   struct FROST_HashCode enc_key_hash;
     54 
     55   /**
     56    *
     57   */
     58   struct FROST_MessageHash message_hash;
     59 
     60   /**
     61    *
     62   */
     63   const char *auth_method;
     64 
     65   /**
     66    *
     67   */
     68   struct GNUNET_HashCode auth_data;
     69 
     70   /**
     71    *
     72   */
     73   bool auth_data_parsed;
     74 
     75   /**
     76    *
     77   */
     78   struct GNUNET_CRYPTO_Edx25519Signature auth_sig;
     79 
     80   /**
     81    *
     82   */
     83   bool auth_sig_parsed;
     84 
     85   /**
     86    *
     87   */
     88   struct GNUNET_CRYPTO_Edx25519PublicKey auth_pub;
     89 
     90   /**
     91    *
     92   */
     93   bool auth_pub_parsed;
     94 
     95   /**
     96    *
     97   */
     98   struct FROSIX_SigRequestIdP id;
     99 
    100   /**
    101    * Reference to the authorization plugin which was loaded
    102   */
    103   struct FROSIX_AuthorizationPlugin *authorization;
    104 
    105 
    106   /**
    107    * Our handler context
    108   */
    109   struct TM_HandlerContext *hc;
    110 
    111   /**
    112    * Uploaded JSON data, NULL if upload is not yet complete.
    113   */
    114   json_t *json;
    115 
    116   /**
    117    * Post parser context.
    118    */
    119   void *post_ctx;
    120 
    121   /**
    122    * Connection handle for closing or resuming
    123    */
    124   struct MHD_Connection *connection;
    125 };
    126 
    127 
    128 static MHD_RESULT
    129 return_sig_commitment (struct SignCommitmentContext *dc,
    130                        struct MHD_Connection *connection)
    131 {
    132   /* generate random seed */
    133   struct FROST_CommitmentSeed seed;
    134   FROST_get_random_seed (&seed);
    135 
    136   /* compute commitments */
    137   struct FROST_Nonce com_nonce;
    138   struct FROST_Commitment commitment;
    139   FROST_generate_nonce_and_commitment (&com_nonce,
    140                                        &commitment,
    141                                        &dc->message_hash,
    142                                        &seed);
    143 
    144   /* compute commitment_id for storing in db */
    145   struct GNUNET_HashCode db_id;
    146   FROSIX_compute_db_commitment_id (&db_id,
    147                                    &dc->enc_key_hash,
    148                                    &commitment);
    149 
    150   /* store seed in db */
    151   enum GNUNET_DB_QueryStatus qs;
    152   qs = db->store_commitment_seed (db->cls,
    153                                   &db_id,
    154                                   &seed);
    155 
    156   switch (qs)
    157   {
    158   case GNUNET_DB_STATUS_HARD_ERROR:
    159   case GNUNET_DB_STATUS_SOFT_ERROR:
    160   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    161     GNUNET_break (0);
    162     return TALER_MHD_reply_with_error (dc->connection,
    163                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
    164                                        TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
    165                                        "store commitment seed");
    166   case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    167     break;
    168   }
    169 
    170   /* parse commitments to json */
    171   return TALER_MHD_REPLY_JSON_PACK (
    172     connection,
    173     MHD_HTTP_OK,
    174     GNUNET_JSON_pack_data_auto ("hiding_commitment",
    175                                 &commitment.hiding_commitment),
    176     GNUNET_JSON_pack_data_auto ("binding_commitment",
    177                                 &commitment.binding_commitment));
    178 }
    179 
    180 
    181 /**
    182  * Generate a response telling the client that answering this
    183  * challenge failed because the rate limit has been exceeded.
    184  *
    185  * @param gc request to answer for
    186  * @return MHD status code
    187  */
    188 static MHD_RESULT
    189 reply_rate_limited (const struct SignCommitmentContext *gc)
    190 {
    191   if (NULL != gc->authorization)
    192     return TALER_MHD_REPLY_JSON_PACK (
    193       gc->connection,
    194       MHD_HTTP_TOO_MANY_REQUESTS,
    195       TALER_MHD_PACK_EC (TALER_EC_GENERIC_TIMEOUT),
    196       GNUNET_JSON_pack_uint64 ("request_limit",
    197                                gc->authorization->retry_counter),
    198       GNUNET_JSON_pack_time_rel ("request_frequency",
    199                                  gc->authorization->code_rotation_period));
    200   /* must be security question */
    201   return TALER_MHD_REPLY_JSON_PACK (
    202     gc->connection,
    203     MHD_HTTP_TOO_MANY_REQUESTS,
    204     TALER_MHD_PACK_EC (TALER_EC_GENERIC_TIMEOUT),
    205     GNUNET_JSON_pack_uint64 ("request_limit",
    206                              INITIAL_RETRY_COUNTER),
    207     GNUNET_JSON_pack_time_rel ("request_frequency",
    208                                MAX_QUESTION_FREQ));
    209 }
    210 
    211 
    212 /**
    213  * Use the database to rate-limit queries to the authentication
    214  * procedure, but without actually storing 'real' challenge codes.
    215  *
    216  * @param[in,out] gc context to rate limit requests for
    217  * @return #GNUNET_OK if rate-limiting passes,
    218  *         #GNUNET_NO if a reply was sent (rate limited)
    219  *         #GNUNET_SYSERR if we failed and no reply
    220  *                        was queued
    221  */
    222 static enum GNUNET_GenericReturnValue
    223 rate_limit (struct SignCommitmentContext *gc)
    224 {
    225   enum GNUNET_DB_QueryStatus qs;
    226   struct GNUNET_TIME_Timestamp rt;
    227   uint64_t code;
    228   enum FROSIX_DB_CodeStatus cs;
    229   bool satisfied;
    230   uint64_t dummy;
    231 
    232   struct FROSIX_ChallengeIdP challenge_id;
    233   GNUNET_CRYPTO_hash (&gc->id,
    234                       sizeof (gc->id),
    235                       &challenge_id.hash);
    236 
    237   rt = GNUNET_TIME_UNIT_FOREVER_TS;
    238   qs = db->create_challenge_code (db->cls,
    239                                   &challenge_id,
    240                                   MAX_QUESTION_FREQ,
    241                                   GNUNET_TIME_UNIT_HOURS,
    242                                   INITIAL_RETRY_COUNTER,
    243                                   &rt,
    244                                   &code);
    245   if (0 > qs)
    246   {
    247     GNUNET_break (0 < qs);
    248     return (MHD_YES ==
    249             TALER_MHD_reply_with_error (gc->connection,
    250                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    251                                         TALER_EC_GENERIC_DB_FETCH_FAILED,
    252                                         "create_challenge_code (for rate limiting)"))
    253            ? GNUNET_NO
    254            : GNUNET_SYSERR;
    255   }
    256   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
    257   {
    258     return (MHD_YES ==
    259             reply_rate_limited (gc))
    260            ? GNUNET_NO
    261            : GNUNET_SYSERR;
    262   }
    263   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    264               "Using intentionally wrong answer to produce rate-limiting\n");
    265   /* decrement trial counter */
    266   cs = db->verify_challenge_code (db->cls,
    267                                   &challenge_id,
    268                                   &challenge_id.hash, /* always use wrong answer
    269                                                          */
    270                                   &dummy,
    271                                   &satisfied);
    272   switch (cs)
    273   {
    274   case FROSIX_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH:
    275     /* good, what we wanted */
    276     return GNUNET_OK;
    277   case FROSIX_DB_CODE_STATUS_HARD_ERROR:
    278   case FROSIX_DB_CODE_STATUS_SOFT_ERROR:
    279     GNUNET_break (0);
    280     return (MHD_YES ==
    281             TALER_MHD_reply_with_error (gc->connection,
    282                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    283                                         TALER_EC_GENERIC_DB_FETCH_FAILED,
    284                                         "verify_challenge_code"))
    285            ? GNUNET_NO
    286            : GNUNET_SYSERR;
    287   case FROSIX_DB_CODE_STATUS_NO_RESULTS:
    288     return (MHD_YES ==
    289             reply_rate_limited (gc))
    290            ? GNUNET_NO
    291            : GNUNET_SYSERR;
    292   case FROSIX_DB_CODE_STATUS_VALID_CODE_STORED:
    293     /* this should be impossible, we used code+1 */
    294     GNUNET_assert (0);
    295   }
    296   return GNUNET_SYSERR;
    297 }
    298 
    299 
    300 /**
    301  * Handle special case of a security question where we do not
    302  * generate a code. Rate limits answers against brute forcing.
    303  *
    304  * @param[in,out] gc request to handle
    305  * @param decrypted_truth hash to check against
    306  * @param decrypted_truth_size number of bytes in @a decrypted_truth
    307  * @return MHD status code
    308  */
    309 static MHD_RESULT
    310 handle_security_question (struct SignCommitmentContext *gc)
    311 {
    312   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    313               "Handling security question challenge\n");
    314   /* rate limit */
    315   /*{
    316     enum GNUNET_GenericReturnValue ret;
    317 
    318     ret = rate_limit (gc);
    319     if (GNUNET_OK != ret)
    320       return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
    321   }*/
    322   /* check reply matches truth */
    323   /* get auth data from DB */
    324   struct FROSIX_ChallengeHashP auth_hash;
    325 
    326   enum GNUNET_DB_QueryStatus qs;
    327   qs = db->get_auth_hash (db->cls,
    328                           &gc->enc_key_hash,
    329                           &auth_hash);
    330   switch (qs)
    331   {
    332   case GNUNET_DB_STATUS_HARD_ERROR:
    333   case GNUNET_DB_STATUS_SOFT_ERROR:
    334   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    335     GNUNET_break (0);
    336     /* FROSIX_EC_KEY_NOT_FOUND */
    337     return TALER_MHD_reply_with_error (gc->connection,
    338                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
    339                                        TALER_EC_GENERIC_DB_FETCH_FAILED,
    340                                        "auth_data_select");
    341   case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    342     break;
    343   }
    344 
    345   /* check if hash of submitted public key matches our stored hash */
    346   struct GNUNET_HashCode db_auth_answer;
    347   struct GNUNET_HashContext *hc = GNUNET_CRYPTO_hash_context_start ();
    348   GNUNET_CRYPTO_hash_context_read (hc,
    349                                    &gc->auth_pub,
    350                                    sizeof (gc->auth_pub));
    351   GNUNET_CRYPTO_hash_context_read (hc,
    352                                    "FROSIX",
    353                                    strlen ("FROSIX"));
    354   GNUNET_CRYPTO_hash_context_finish (hc,
    355                                      &db_auth_answer);
    356 
    357   if (GNUNET_OK != FROSIX_gnunet_hash_cmp (&db_auth_answer,
    358                                            &auth_hash.hash))
    359   {
    360     GNUNET_break (0);
    361     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    362                 "Authentication public key not matching\n");
    363     return TALER_MHD_reply_with_error (gc->connection,
    364                                        MHD_HTTP_FORBIDDEN,
    365                                        TALER_EC_GENERIC_INVALID_RESPONSE,
    366                                        NULL);
    367   }
    368 
    369   /* check signature of message hash with submitted public key */
    370   struct FROSIX_AuthSignaturePS as = {
    371     .purpose.purpose = htonl (72),
    372     .purpose.size = htonl (sizeof (as)),
    373     .mh = gc->message_hash,
    374   };
    375 
    376   if (GNUNET_OK != GNUNET_CRYPTO_edx25519_verify (72,
    377                                                   &as,
    378                                                   &gc->auth_sig,
    379                                                   &gc->auth_pub))
    380   {
    381     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    382                 "Signature of message not verified\n");
    383     return TALER_MHD_reply_with_error (gc->connection,
    384                                        MHD_HTTP_FORBIDDEN,
    385                                        TALER_EC_GENERIC_INVALID_RESPONSE,
    386                                        NULL);
    387   }
    388 
    389   /* good, return the key share */
    390   return return_sig_commitment (gc,
    391                                 gc->connection);
    392 }
    393 
    394 
    395 MHD_RESULT
    396 FH_handler_sig_commitment_post (
    397   struct MHD_Connection *connection,
    398   struct TM_HandlerContext *hc,
    399   const struct FROSIX_SigRequestIdP *id,
    400   const char *sign_commitment_data,
    401   size_t *sign_commitment_data_size)
    402 {
    403   enum GNUNET_GenericReturnValue res;
    404   struct SignCommitmentContext *dc = hc->ctx;
    405 
    406   if (NULL == dc)
    407   {
    408     dc = GNUNET_new (struct SignCommitmentContext);
    409     dc->connection = connection;
    410     dc->id = *id;
    411     hc->ctx = dc;
    412   }
    413 
    414   /* parse request body */
    415   if (NULL == dc->json)
    416   {
    417     res = TALER_MHD_parse_post_json (dc->connection,
    418                                      &dc->post_ctx,
    419                                      sign_commitment_data,
    420                                      sign_commitment_data_size,
    421                                      &dc->json);
    422     if (GNUNET_SYSERR == res)
    423     {
    424       GNUNET_break (0);
    425       return MHD_NO;
    426     }
    427     if ((GNUNET_NO == res ||
    428          (NULL == dc->json)))
    429     {
    430       return MHD_YES;
    431     }
    432   }
    433   struct GNUNET_JSON_Specification spec[] = {
    434     GNUNET_JSON_spec_fixed_auto ("message_hash",
    435                                  &dc->message_hash),
    436     GNUNET_JSON_spec_fixed_auto ("encryption_key_hash",
    437                                  &dc->enc_key_hash),
    438     GNUNET_JSON_spec_string ("auth_method",
    439                              &dc->auth_method),
    440     GNUNET_JSON_spec_mark_optional (
    441       GNUNET_JSON_spec_fixed_auto ("auth_data",
    442                                    &dc->auth_data),
    443       &dc->auth_data_parsed),
    444     GNUNET_JSON_spec_mark_optional (
    445       GNUNET_JSON_spec_fixed_auto ("auth_sig",
    446                                    &dc->auth_sig),
    447       &dc->auth_sig_parsed),
    448     GNUNET_JSON_spec_mark_optional (
    449       GNUNET_JSON_spec_fixed_auto ("auth_pub",
    450                                    &dc->auth_pub),
    451       &dc->auth_pub_parsed),
    452     GNUNET_JSON_spec_end ()
    453   };
    454 
    455   res = TALER_MHD_parse_json_data (connection,
    456                                    dc->json,
    457                                    spec);
    458 
    459   if (GNUNET_SYSERR == res)
    460   {
    461     GNUNET_JSON_parse_free (spec);
    462     GNUNET_break (0);
    463     return MHD_NO;
    464   }
    465   if (GNUNET_NO == res)
    466   {
    467     GNUNET_JSON_parse_free (spec);
    468     GNUNET_break_op (0);
    469     /* FROSIX_EC_PARAMETER_MALFORMED */
    470     return TALER_MHD_reply_with_error (connection,
    471                                        MHD_HTTP_BAD_REQUEST,
    472                                        TALER_EC_GENERIC_PARAMETER_MALFORMED,
    473                                        "Unable to parse request body");
    474   }
    475 
    476   GNUNET_JSON_parse_free (spec);
    477 
    478   /* validate id */
    479   {
    480     struct FROSIX_SigRequestIdP req_id;
    481     FROSIX_compute_signature_request_id (&req_id,
    482                                          &dc->enc_key_hash,
    483                                          &dc->message_hash);
    484     if (GNUNET_NO == FROST_hash_cmp (&req_id.id,
    485                                      &id->id))
    486     {
    487       GNUNET_JSON_parse_free (spec);
    488       /* FROSIX_EC_REQUEST_ID_NOT_MATCHING */
    489       GNUNET_break_op (0);
    490       return TALER_MHD_reply_with_error (connection,
    491                                          MHD_HTTP_BAD_REQUEST,
    492                                          TALER_EC_GENERIC_PARAMETER_MALFORMED,
    493                                          "ID in URL not matching data in body");
    494     }
    495   }
    496 
    497   /* authenticate */
    498   if (0 == strcmp ("question",
    499                    dc->auth_method))
    500   {
    501     if (&dc->auth_sig_parsed && &dc->auth_pub_parsed)
    502     {
    503       return handle_security_question (dc);
    504     }
    505     else
    506     {
    507       GNUNET_break (0);
    508       return TALER_MHD_reply_with_error (connection,
    509                                          MHD_HTTP_BAD_REQUEST,
    510                                          TALER_EC_GENERIC_PARAMETER_MALFORMED,
    511                                          NULL);
    512     }
    513   }
    514   else
    515   {
    516     if (! &dc->auth_data_parsed)
    517     {
    518       GNUNET_break (0);
    519       return TALER_MHD_reply_with_error (connection,
    520                                          MHD_HTTP_BAD_REQUEST,
    521                                          TALER_EC_GENERIC_PARAMETER_MALFORMED,
    522                                          NULL);
    523     }
    524 
    525     enum FROSIX_DB_CodeStatus cs;
    526     bool satisfied = false;
    527     uint64_t code;
    528 
    529     struct FROSIX_ChallengeIdP challenge_id;
    530     FROSIX_compute_challenge_id (&challenge_id,
    531                                  &dc->enc_key_hash,
    532                                  &dc->message_hash);
    533 
    534     /* random code, check against database */
    535     cs = db->verify_challenge_code (db->cls,
    536                                     &challenge_id,
    537                                     &dc->auth_data,
    538                                     &code,
    539                                     &satisfied);
    540     switch (cs)
    541     {
    542     case FROSIX_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH:
    543       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    544                   "Provided response does not match our stored challenge\n");
    545       return TALER_MHD_reply_with_error (connection,
    546                                          MHD_HTTP_FORBIDDEN,
    547                                          TALER_EC_GENERIC_DB_FETCH_FAILED,
    548                                          NULL);
    549     case FROSIX_DB_CODE_STATUS_HARD_ERROR:
    550     case FROSIX_DB_CODE_STATUS_SOFT_ERROR:
    551       GNUNET_break (0);
    552       return TALER_MHD_reply_with_error (dc->connection,
    553                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
    554                                          TALER_EC_GENERIC_DB_FETCH_FAILED,
    555                                          "verify_challenge_code");
    556     case FROSIX_DB_CODE_STATUS_NO_RESULTS:
    557       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    558                   "Specified challenge code %s was not issued\n",
    559                   GNUNET_h2s (&dc->auth_data));
    560       return TALER_MHD_reply_with_error (connection,
    561                                          MHD_HTTP_FORBIDDEN,
    562                                          TALER_EC_GENERIC_DB_FETCH_FAILED,
    563                                          "specific challenge code was not issued");
    564     case FROSIX_DB_CODE_STATUS_VALID_CODE_STORED:
    565       return return_sig_commitment (dc,
    566                                     connection);
    567     default:
    568       GNUNET_break (0);
    569       return MHD_NO;
    570     }
    571   }
    572 }