anastasis_authorization_plugin_sms.c (22465B)
1 /* 2 This file is part of Anastasis 3 Copyright (C) 2019, 2021 Anastasis SARL 4 5 Anastasis 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 Anastasis 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 Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> 15 */ 16 /** 17 * @file anastasis_authorization_plugin_sms.c 18 * @brief authorization plugin email based 19 * @author Dominik Meister 20 */ 21 #include "platform.h" 22 #include "anastasis_authorization_plugin.h" 23 #include <taler/taler_mhd_lib.h> 24 #include <taler/taler_json_lib.h> 25 #include <regex.h> 26 #include "anastasis_util_lib.h" 27 #include <gnunet/gnunet_db_lib.h> 28 #include "anastasis_database_lib.h" 29 30 /** 31 * How many retries do we allow per code? 32 */ 33 #define INITIAL_RETRY_COUNTER 3 34 35 /** 36 * Saves the State of a authorization plugin. 37 */ 38 struct SMS_Context 39 { 40 41 /** 42 * Command which is executed to run the plugin (some bash script or a 43 * command line argument) 44 */ 45 char *auth_command; 46 47 /** 48 * Regex for phone number validation. 49 */ 50 regex_t regex; 51 52 /** 53 * Messages of the plugin, read from a resource file. 54 */ 55 json_t *messages; 56 57 /** 58 * Context we operate in. 59 */ 60 const struct ANASTASIS_AuthorizationContext *ac; 61 }; 62 63 64 /** 65 * Saves the State of a authorization process 66 */ 67 struct ANASTASIS_AUTHORIZATION_State 68 { 69 /** 70 * Public key of the challenge which is authorised 71 */ 72 struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; 73 74 /** 75 * Code which is sent to the user (here sent via SMS) 76 */ 77 uint64_t code; 78 79 /** 80 * Our plugin context. 81 */ 82 struct SMS_Context *ctx; 83 84 /** 85 * Function to call when we made progress. 86 */ 87 GNUNET_SCHEDULER_TaskCallback trigger; 88 89 /** 90 * Closure for @e trigger. 91 */ 92 void *trigger_cls; 93 94 /** 95 * holds the truth information 96 */ 97 char *phone_number; 98 99 /** 100 * Handle to the helper process. 101 */ 102 struct GNUNET_Process *child; 103 104 /** 105 * Handle to wait for @e child 106 */ 107 struct GNUNET_ChildWaitHandle *cwh; 108 109 /** 110 * Our client connection, set if suspended. 111 */ 112 struct MHD_Connection *connection; 113 114 /** 115 * Message to send. 116 */ 117 char *msg; 118 119 /** 120 * Offset of transmission in msg. 121 */ 122 size_t msg_off; 123 124 /** 125 * Exit code from helper. 126 */ 127 long unsigned int exit_code; 128 129 /** 130 * How did the helper die? 131 */ 132 enum GNUNET_OS_ProcessStatusType pst; 133 134 }; 135 136 137 /** 138 * Returned by #get_message() when the configured messages file has no entry 139 * for the requested ID. This is an installation error, but it must not be 140 * allowed to reach GNUNET_asprintf() as a NULL format string. The text 141 * deliberately contains no printf conversions, so substituting it for any 142 * template is safe whatever argument list the caller passes. 143 */ 144 #define MISSING_MESSAGE \ 145 "The provider is misconfigured: a message template is missing." 146 147 148 /** 149 * Obtain internationalized message @a msg_id from @a ctx using 150 * language preferences of @a conn. 151 * 152 * @param messages JSON object to lookup message from 153 * @param conn connection to lookup message for 154 * @param msg_id unique message ID 155 * @return the requested message, or #MISSING_MESSAGE if it was not 156 * configured; never NULL 157 */ 158 static const char * 159 get_message (const json_t *messages, 160 struct MHD_Connection *conn, 161 const char *msg_id) 162 { 163 const char *accept_lang; 164 165 accept_lang = MHD_lookup_connection_value (conn, 166 MHD_HEADER_KIND, 167 MHD_HTTP_HEADER_ACCEPT_LANGUAGE); 168 if (NULL == accept_lang) 169 accept_lang = "en_US"; 170 { 171 const char *ret; 172 struct GNUNET_JSON_Specification spec[] = { 173 TALER_JSON_spec_i18n_string (msg_id, 174 accept_lang, 175 &ret), 176 GNUNET_JSON_spec_end () 177 }; 178 179 if (GNUNET_OK != 180 GNUNET_JSON_parse (messages, 181 spec, 182 NULL, NULL)) 183 { 184 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 185 "Message `%s' is missing from the configured messages file\n", 186 msg_id); 187 GNUNET_JSON_parse_free (spec); 188 return MISSING_MESSAGE; 189 } 190 GNUNET_JSON_parse_free (spec); 191 if (NULL == ret) 192 { 193 /* The parser of TALER_JSON_spec_i18n_string returns #GNUNET_OK even 194 when the field is absent or is not a string, in which case it stores 195 NULL; the check above therefore never fires for a missing message and 196 this one is what keeps NULL out of the format argument of the 197 GNUNET_asprintf() calls below. */ 198 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 199 "Message `%s' is missing or not a string in the configured messages file\n", 200 msg_id); 201 return MISSING_MESSAGE; 202 } 203 return ret; 204 } 205 } 206 207 208 /** 209 * Validate @a data is a well-formed input into the challenge method, 210 * i.e. @a data is a well-formed phone number for sending an SMS, or 211 * a well-formed e-mail address for sending an e-mail. Not expected to 212 * check that the phone number or e-mail account actually exists. 213 * 214 * To be possibly used before issuing a 402 payment required to the client. 215 * 216 * @param cls closure with a `struct SMS_Context` 217 * @param connection HTTP client request (for queuing response) 218 * @param truth_mime mime type of @e data 219 * @param data input to validate (i.e. is it a valid phone number, etc.) 220 * @param data_length number of bytes in @a data 221 * @return #GNUNET_OK if @a data is valid, 222 * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection 223 * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection 224 */ 225 static enum GNUNET_GenericReturnValue 226 sms_validate (void *cls, 227 struct MHD_Connection *connection, 228 const char *truth_mime, 229 const char *data, 230 size_t data_length) 231 { 232 struct SMS_Context *ctx = cls; 233 int regex_result; 234 char *phone_number; 235 236 phone_number = GNUNET_strndup (data, 237 data_length); 238 regex_result = regexec (&ctx->regex, 239 phone_number, 240 0, 241 NULL, 242 0); 243 GNUNET_free (phone_number); 244 if (0 != regex_result) 245 { 246 if (MHD_NO == 247 TALER_MHD_reply_with_error (connection, 248 MHD_HTTP_CONFLICT, 249 TALER_EC_ANASTASIS_SMS_PHONE_INVALID, 250 NULL)) 251 return GNUNET_SYSERR; 252 return GNUNET_NO; 253 } 254 return GNUNET_OK; 255 } 256 257 258 /** 259 * Begin issuing authentication challenge to user based on @a data. 260 * Sends SMS. 261 * 262 * @param cls closure with a `struct SMS_Context` 263 * @param trigger function to call when we made progress 264 * @param trigger_cls closure for @a trigger 265 * @param truth_uuid Identifier of the challenge, to be (if possible) included in the 266 * interaction with the user 267 * @param code secret code that the user has to provide back to satisfy the challenge in 268 * the main anastasis protocol 269 * @param data input to validate (i.e. is it a valid phone number, etc.) 270 * @param data_length number of bytes in @a data 271 * @return state to track progress on the authorization operation, NULL on failure 272 */ 273 static struct ANASTASIS_AUTHORIZATION_State * 274 sms_start (void *cls, 275 GNUNET_SCHEDULER_TaskCallback trigger, 276 void *trigger_cls, 277 const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, 278 uint64_t code, 279 const void *data, 280 size_t data_length) 281 { 282 struct SMS_Context *ctx = cls; 283 struct ANASTASIS_AUTHORIZATION_State *as; 284 enum GNUNET_DB_QueryStatus qs; 285 286 /* If the user can show this challenge code, this 287 plugin is already happy (no additional 288 requirements), so mark this challenge as 289 already satisfied from the start. */ 290 qs = ANASTASIS_DB_update_to_challenge_code_satisfied ( 291 truth_uuid, 292 code); 293 if (qs <= 0) 294 { 295 GNUNET_break (0); 296 return NULL; 297 } 298 as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); 299 as->trigger = trigger; 300 as->trigger_cls = trigger_cls; 301 as->ctx = ctx; 302 as->truth_uuid = *truth_uuid; 303 as->code = code; 304 as->phone_number = GNUNET_strndup (data, 305 data_length); 306 return as; 307 } 308 309 310 /** 311 * Map the exit status of the SMS helper onto an HTTP status and error code. 312 * 313 * The helpers shipped with Challenger use a banded exit-code scheme documented 314 * in challenger-send-sms(1): anything below 10 means the challenge was 315 * transmitted (0 confirmed on the handset, 1 accepted by the provider, 2 316 * suppressed as a duplicate of a message already in flight), 10-19 blames the 317 * address the user gave us, 20-29 is a recipient that is temporarily 318 * unreachable, 30-39 is the transmission provider and 40-49 is our own 319 * configuration. 320 * 321 * Codes we do not recognise are reported as an upstream failure rather than 322 * blamed on the user, so that a helper predating this scheme -- which used 323 * small ad-hoc exit codes -- never yields a client error. 324 * 325 * @param exit_code exit status of the helper, which must have exited normally 326 * @param[out] http_status set to the HTTP status to return 327 * @return error code to return, #TALER_EC_NONE if the challenge was transmitted 328 */ 329 static enum TALER_ErrorCode 330 classify_helper_status (unsigned long int exit_code, 331 unsigned int *http_status) 332 { 333 if (exit_code < 10) 334 { 335 *http_status = MHD_HTTP_OK; 336 return TALER_EC_NONE; 337 } 338 if (exit_code < 20) 339 { 340 *http_status = MHD_HTTP_CONFLICT; 341 return TALER_EC_ANASTASIS_SMS_PHONE_INVALID; 342 } 343 if (exit_code < 30) 344 { 345 *http_status = MHD_HTTP_SERVICE_UNAVAILABLE; 346 return TALER_EC_ANASTASIS_ADDRESS_UNREACHABLE; 347 } 348 if ( (exit_code >= 40) && 349 (exit_code < 50) ) 350 { 351 *http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; 352 return TALER_EC_ANASTASIS_HELPER_MISCONFIGURED; 353 } 354 *http_status = MHD_HTTP_BAD_GATEWAY; 355 return TALER_EC_ANASTASIS_SMS_HELPER_COMMAND_FAILED; 356 } 357 358 359 /** 360 * Function called when our SMS helper has terminated. 361 * 362 * @param cls our `struct ANASTASIS_AUHTORIZATION_State` 363 * @param type type of the process 364 * @param exit_code status code of the process 365 */ 366 static void 367 sms_done_cb (void *cls, 368 enum GNUNET_OS_ProcessStatusType type, 369 long unsigned int exit_code) 370 { 371 struct ANASTASIS_AUTHORIZATION_State *as = cls; 372 373 as->cwh = NULL; 374 if (NULL != as->child) 375 { 376 GNUNET_process_destroy (as->child); 377 as->child = NULL; 378 } 379 as->pst = type; 380 as->exit_code = exit_code; 381 MHD_resume_connection (as->connection); 382 as->trigger (as->trigger_cls); 383 } 384 385 386 /** 387 * Begin issuing authentication challenge to user based on @a data. 388 * I.e. start to send SMS or e-mail or launch video identification. 389 * 390 * @param as authorization state 391 * @param connection HTTP client request (for queuing response, such as redirection to video portal) 392 * @return state of the request 393 */ 394 static enum ANASTASIS_AUTHORIZATION_ChallengeResult 395 sms_challenge (struct ANASTASIS_AUTHORIZATION_State *as, 396 struct MHD_Connection *connection) 397 { 398 enum MHD_Result mres; 399 const char *mime; 400 const char *lang; 401 402 mime = MHD_lookup_connection_value (connection, 403 MHD_HEADER_KIND, 404 MHD_HTTP_HEADER_ACCEPT); 405 if (NULL == mime) 406 mime = "text/plain"; 407 lang = MHD_lookup_connection_value (connection, 408 MHD_HEADER_KIND, 409 MHD_HTTP_HEADER_ACCEPT_LANGUAGE); 410 if (NULL == lang) 411 lang = "en"; 412 if (NULL == as->msg) 413 { 414 /* First time, start child process and feed pipe */ 415 struct GNUNET_DISK_PipeHandle *p; 416 struct GNUNET_DISK_FileHandle *pipe_stdin; 417 418 p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW); 419 if (NULL == p) 420 { 421 mres = TALER_MHD_reply_with_error (connection, 422 MHD_HTTP_INTERNAL_SERVER_ERROR, 423 TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED, 424 "pipe"); 425 if (MHD_YES != mres) 426 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 427 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 428 } 429 as->child = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR); 430 GNUNET_assert (GNUNET_OK == 431 GNUNET_process_set_options ( 432 as->child, 433 GNUNET_process_option_inherit_rpipe (p, 434 STDIN_FILENO))); 435 if (GNUNET_OK != 436 GNUNET_process_run_command_va (as->child, 437 as->ctx->auth_command, 438 as->ctx->auth_command, 439 as->phone_number, 440 NULL)) 441 { 442 GNUNET_process_destroy (as->child); 443 as->child = NULL; 444 GNUNET_DISK_pipe_close (p); 445 mres = TALER_MHD_reply_with_error (connection, 446 MHD_HTTP_INTERNAL_SERVER_ERROR, 447 TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED, 448 "exec"); 449 if (MHD_YES != mres) 450 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 451 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 452 } 453 pipe_stdin = GNUNET_DISK_pipe_detach_end (p, 454 GNUNET_DISK_PIPE_END_WRITE); 455 GNUNET_assert (NULL != pipe_stdin); 456 GNUNET_DISK_pipe_close (p); 457 GNUNET_asprintf (&as->msg, 458 "%s\nAnastasis:\n%s", 459 ANASTASIS_pin2s (as->code), 460 ANASTASIS_CRYPTO_uuid2s (&as->truth_uuid)); 461 { 462 const char *off = as->msg; 463 size_t left = strlen (off); 464 465 while (0 != left) 466 { 467 ssize_t ret; 468 469 ret = GNUNET_DISK_file_write (pipe_stdin, 470 off, 471 left); 472 if (ret <= 0) 473 { 474 mres = TALER_MHD_reply_with_error (connection, 475 MHD_HTTP_INTERNAL_SERVER_ERROR, 476 TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED, 477 "write"); 478 if (MHD_YES != mres) 479 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 480 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 481 } 482 as->msg_off += ret; 483 off += ret; 484 left -= ret; 485 } 486 GNUNET_DISK_file_close (pipe_stdin); 487 } 488 as->cwh = GNUNET_wait_child (as->child, 489 &sms_done_cb, 490 as); 491 as->connection = connection; 492 MHD_suspend_connection (connection); 493 return ANASTASIS_AUTHORIZATION_CRES_SUSPENDED; 494 } 495 if (NULL != as->cwh) 496 { 497 /* Spurious call, why are we here? */ 498 GNUNET_break (0); 499 MHD_suspend_connection (connection); 500 return ANASTASIS_AUTHORIZATION_CRES_SUSPENDED; 501 } 502 if ( (GNUNET_OS_PROCESS_EXITED != as->pst) || 503 (as->exit_code >= 10) ) 504 { 505 char es[32]; 506 unsigned int http_status; 507 enum TALER_ErrorCode ec; 508 509 if (GNUNET_OS_PROCESS_EXITED != as->pst) 510 { 511 /* Killed by a signal or otherwise abnormal: there is no exit code to 512 classify. */ 513 http_status = MHD_HTTP_BAD_GATEWAY; 514 ec = TALER_EC_ANASTASIS_SMS_HELPER_COMMAND_FAILED; 515 } 516 else 517 { 518 ec = classify_helper_status (as->exit_code, 519 &http_status); 520 } 521 GNUNET_snprintf (es, 522 sizeof (es), 523 "%u/%d", 524 (unsigned int) as->exit_code, 525 as->pst); 526 mres = TALER_MHD_reply_with_error (connection, 527 http_status, 528 ec, 529 es); 530 if (MHD_YES != mres) 531 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 532 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 533 } 534 535 /* Build HTTP response */ 536 { 537 struct MHD_Response *resp; 538 const char *end; 539 size_t slen; 540 541 slen = strlen (as->phone_number); 542 if (slen > 4) 543 end = &as->phone_number[slen - 4]; 544 else 545 end = &as->phone_number[slen / 2]; 546 547 if (0.0 < TALER_pattern_matches (mime, 548 "application/json")) 549 { 550 resp = TALER_MHD_MAKE_JSON_PACK ( 551 GNUNET_JSON_pack_string ("challenge_type", 552 "TAN_SENT"), 553 GNUNET_JSON_pack_string ("tan_address_hint", 554 end)); 555 } 556 else 557 { 558 size_t reply_len; 559 char *reply; 560 561 reply_len = GNUNET_asprintf (&reply, 562 get_message (as->ctx->messages, 563 connection, 564 "instructions"), 565 end); 566 resp = MHD_create_response_from_buffer (reply_len, 567 reply, 568 MHD_RESPMEM_MUST_COPY); 569 GNUNET_free (reply); 570 TALER_MHD_add_global_headers (resp, 571 false); 572 GNUNET_break (MHD_YES == 573 MHD_add_response_header (resp, 574 MHD_HTTP_HEADER_CONTENT_TYPE, 575 "text/plain")); 576 } 577 mres = MHD_queue_response (connection, 578 MHD_HTTP_OK, 579 resp); 580 MHD_destroy_response (resp); 581 if (MHD_YES != mres) 582 return ANASTASIS_AUTHORIZATION_CRES_SUCCESS_REPLY_FAILED; 583 return ANASTASIS_AUTHORIZATION_CRES_SUCCESS; 584 } 585 } 586 587 588 /** 589 * Free internal state associated with @a as. 590 * 591 * @param as state to clean up 592 */ 593 static void 594 sms_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) 595 { 596 if (NULL != as->cwh) 597 { 598 GNUNET_wait_child_cancel (as->cwh); 599 as->cwh = NULL; 600 } 601 if (NULL != as->child) 602 { 603 GNUNET_break (GNUNET_OK == 604 GNUNET_process_kill (as->child, 605 SIGKILL)); 606 GNUNET_break (GNUNET_OK == 607 GNUNET_process_wait (as->child, 608 true, 609 NULL, 610 NULL)); 611 GNUNET_process_destroy (as->child); 612 as->child = NULL; 613 } 614 GNUNET_free (as->msg); 615 GNUNET_free (as->phone_number); 616 GNUNET_free (as); 617 } 618 619 620 /** 621 * Initialize email based authorization plugin 622 * 623 * @param cls a configuration instance 624 * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` 625 */ 626 void * 627 libanastasis_plugin_authorization_sms_init (void *cls); 628 629 /* declaration to fix compiler warning */ 630 void * 631 libanastasis_plugin_authorization_sms_init (void *cls) 632 { 633 const struct ANASTASIS_AuthorizationContext *ac = cls; 634 struct ANASTASIS_AuthorizationPlugin *plugin; 635 const struct GNUNET_CONFIGURATION_Handle *cfg = ac->cfg; 636 struct SMS_Context *ctx; 637 638 ctx = GNUNET_new (struct SMS_Context); 639 ctx->ac = ac; 640 { 641 char *fn; 642 json_error_t err; 643 char *tmp; 644 645 tmp = GNUNET_OS_installation_get_path (ANASTASIS_project_data (), 646 GNUNET_OS_IPK_DATADIR); 647 GNUNET_asprintf (&fn, 648 "%sauthorization-sms-messages.json", 649 tmp); 650 GNUNET_free (tmp); 651 ctx->messages = json_load_file (fn, 652 JSON_REJECT_DUPLICATES, 653 &err); 654 if (NULL == ctx->messages) 655 { 656 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 657 "Failed to load messages from `%s': %s at %d:%d\n", 658 fn, 659 err.text, 660 err.line, 661 err.column); 662 GNUNET_free (fn); 663 GNUNET_free (ctx); 664 return NULL; 665 } 666 GNUNET_free (fn); 667 } 668 { 669 int regex_result; 670 const char *regexp = "^\\+?[0-9]+$"; 671 672 regex_result = regcomp (&ctx->regex, 673 regexp, 674 REG_EXTENDED); 675 if (0 != regex_result) 676 { 677 GNUNET_break (0); 678 json_decref (ctx->messages); 679 GNUNET_free (ctx); 680 return NULL; 681 } 682 } 683 plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); 684 plugin->retry_counter = INITIAL_RETRY_COUNTER; 685 plugin->code_validity_period = GNUNET_TIME_UNIT_DAYS; 686 plugin->code_rotation_period = GNUNET_TIME_UNIT_HOURS; 687 plugin->code_retransmission_frequency = GNUNET_TIME_UNIT_MINUTES; 688 plugin->cls = ctx; 689 plugin->validate = &sms_validate; 690 plugin->start = &sms_start; 691 plugin->challenge = &sms_challenge; 692 plugin->cleanup = &sms_cleanup; 693 694 if (GNUNET_OK != 695 GNUNET_CONFIGURATION_get_value_string (cfg, 696 "authorization-sms", 697 "COMMAND", 698 &ctx->auth_command)) 699 { 700 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, 701 "authorization-sms", 702 "COMMAND"); 703 regfree (&ctx->regex); 704 json_decref (ctx->messages); 705 GNUNET_free (ctx); 706 GNUNET_free (plugin); 707 return NULL; 708 } 709 return plugin; 710 } 711 712 713 /** 714 * Unload authorization plugin 715 * 716 * @param cls a `struct ANASTASIS_AuthorizationPlugin` 717 * @return NULL (always) 718 */ 719 void * 720 libanastasis_plugin_authorization_sms_done (void *cls); 721 722 /* declaration to fix compiler warning */ 723 void * 724 libanastasis_plugin_authorization_sms_done (void *cls) 725 { 726 struct ANASTASIS_AuthorizationPlugin *plugin = cls; 727 struct SMS_Context *ctx = plugin->cls; 728 729 GNUNET_free (ctx->auth_command); 730 regfree (&ctx->regex); 731 json_decref (ctx->messages); 732 GNUNET_free (ctx); 733 GNUNET_free (plugin); 734 return NULL; 735 }