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 }