From f2dbb45c194cfc41395b7a13a828e24ceeb7bc89 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Sun, 26 Sep 2021 19:24:14 +0200 Subject: starting with TOTP plugin for #7023 --- contrib/gana | 2 +- src/authorization/Makefile.am | 21 +- .../anastasis_authorization_plugin_file.c | 4 +- .../anastasis_authorization_plugin_totp.c | 376 +++++++++++++++++++++ src/include/anastasis_authorization_plugin.h | 8 + 5 files changed, 407 insertions(+), 4 deletions(-) create mode 100644 src/authorization/anastasis_authorization_plugin_totp.c diff --git a/contrib/gana b/contrib/gana index 323cb82..3b63803 160000 --- a/contrib/gana +++ b/contrib/gana @@ -1 +1 @@ -Subproject commit 323cb8276408e2c02b59bbe6e10da904538a149d +Subproject commit 3b638032297cfed132912dfe82a1c47033eff03b diff --git a/src/authorization/Makefile.am b/src/authorization/Makefile.am index cfcd89e..c99c6b4 100644 --- a/src/authorization/Makefile.am +++ b/src/authorization/Makefile.am @@ -69,7 +69,8 @@ plugin_LTLIBRARIES = \ libanastasis_plugin_authorization_file.la \ libanastasis_plugin_authorization_iban.la \ libanastasis_plugin_authorization_post.la \ - libanastasis_plugin_authorization_sms.la + libanastasis_plugin_authorization_sms.la \ + libanastasis_plugin_authorization_totp.la libanastasis_plugin_authorization_file_la_SOURCES = \ @@ -151,3 +152,21 @@ libanastasis_plugin_authorization_sms_la_LDFLAGS = \ -ljansson \ -lmicrohttpd \ $(XLIB) + + +libanastasis_plugin_authorization_totp_la_SOURCES = \ + anastasis_authorization_plugin_totp.c +libanastasis_plugin_authorization_totp_la_LIBADD = \ + $(LTLIBINTL) +libanastasis_plugin_authorization_totp_la_LDFLAGS = \ + $(ANASTASIS_PLUGIN_LDFLAGS) \ + $(top_builddir)/src/stasis/libanastasisdb.la \ + -ltalerjson \ + -ltalermhd \ + -ltalerutil \ + -lgnunetjson \ + -lgnunetutil \ + -ljansson \ + -lmicrohttpd \ + -lgcrypt \ + $(XLIB) diff --git a/src/authorization/anastasis_authorization_plugin_file.c b/src/authorization/anastasis_authorization_plugin_file.c index 66dbbe1..d41eaa3 100644 --- a/src/authorization/anastasis_authorization_plugin_file.c +++ b/src/authorization/anastasis_authorization_plugin_file.c @@ -86,7 +86,7 @@ file_validate (void *cls, (void) cls; if (NULL == data) - return GNUNET_NO; + return GNUNET_SYSERR; filename = GNUNET_STRINGS_data_to_string_alloc (data, data_length); flag = false; @@ -100,7 +100,7 @@ file_validate (void *cls, } } if (flag) - return GNUNET_NO; + return GNUNET_SYSERR; GNUNET_free (filename); return GNUNET_OK; } diff --git a/src/authorization/anastasis_authorization_plugin_totp.c b/src/authorization/anastasis_authorization_plugin_totp.c new file mode 100644 index 0000000..6fcdd39 --- /dev/null +++ b/src/authorization/anastasis_authorization_plugin_totp.c @@ -0,0 +1,376 @@ +/* + This totp is part of Anastasis + Copyright (C) 2021 Anastasis SARL + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + Anastasis; see the totp COPYING.GPL. If not, see +*/ +/** + * @totp anastasis_authorization_plugin_totp.c + * @brief authorization plugin using totp + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis_authorization_plugin.h" +#include +#include +#include "anastasis_database_lib.h" +#include + + +/** + * How many retries do we allow per code? + */ +#define INITIAL_RETRY_COUNTER 3 + +/** + * How long is a TOTP code valid? + */ +#define TOTP_VALIDITY_PERIOD GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_SECONDS, 30) + +/** + * Range of time we allow (plus-minus). + */ +#define TIME_INTERVAL_RANGE 2 + +/** + * How long is the shared secret in bytes? + */ +#define SECRET_LEN 32 + + +/** + * Saves the state of a authorization process + */ +struct ANASTASIS_AUTHORIZATION_State +{ + /** + * UUID of the challenge which is authorised + */ + struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; + + /** + * Our context. + */ + const struct ANASTASIS_AuthorizationContext *ac; + + /** + * Was the challenge satisfied? + */ + bool ok; + +}; + + +/** + * Validate @a data is a well-formed input into the challenge method, + * i.e. @a data is a well-formed phone number for sending an SMS, or + * a well-formed e-mail address for sending an e-mail. Not expected to + * check that the phone number or e-mail account actually exists. + * + * To be possibly used before issuing a 402 payment required to the client. + * + * @param cls closure with a `const struct ANASTASIS_AuthorizationContext *` + * @param connection HTTP client request (for queuing response) + * @param truth_mime mime type of @e data + * @param data input to validate (i.e. is it a valid phone number, etc.) + * @param data_length number of bytes in @a data + * @return #GNUNET_OK if @a data is valid, + * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection + * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection + */ +static enum GNUNET_GenericReturnValue +totp_validate (void *cls, + struct MHD_Connection *connection, + const char *truth_mime, + const char *data, + size_t data_length) +{ + (void) cls; + (void) truth_mime; + (void) connection; + if (NULL == data) + { + GNUNET_break_op (0); + if (MHD_NO == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_EXPECTATION_FAILED, + TALER_EC_ANASTASIS_TOTP_KEY_MISSING, + NULL)) + return GNUNET_SYSERR; + return GNUNET_NO; + } + if (SECRET_LEN != data_length) + { + GNUNET_break_op (0); + if (MHD_NO == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_EXPECTATION_FAILED, + TALER_EC_ANASTASIS_TOTP_KEY_INVALID, + NULL)) + return GNUNET_SYSERR; + return GNUNET_NO; + } + return GNUNET_OK; +} + + +/** + * Compute TOTP code at current time with offset + * @a time_off for the @a key. + * + * @param time_off offset to apply when computing the code + * @param key input key material + * @param key_size number of bytes in @a key + * @return TOTP code at this time + */ +static uint64_t +compute_totp (int time_off, + const void *key, + size_t key_size) +{ + struct GNUNET_TIME_Absolute now; + time_t t; + uint64_t ctr; + uint8_t hmac[16]; /* SHA1: 16 bytes */ + + now = GNUNET_TIME_absolute_get (); + while (time_off < 0) + { + now = GNUNET_TIME_absolute_subtract (now, + TOTP_VALIDITY_PERIOD); + time_off++; + } + while (time_off > 0) + { + now = GNUNET_TIME_absolute_add (now, + TOTP_VALIDITY_PERIOD); + time_off--; + } + t = now.abs_value_us / GNUNET_TIME_UNIT_SECONDS.rel_value_us; + ctr = GNUNET_htonll (t / 30LLU); + + { + gcry_md_hd_t md; + const unsigned char *mc; + + GNUNET_assert (GPG_ERR_NO_ERROR == + gcry_md_open (&md, + GCRY_MD_SHA1, + GCRY_MD_FLAG_HMAC)); + gcry_md_setkey (md, + key, + key_size); + gcry_md_write (md, + &ctr, + sizeof (ctr)); + mc = gcry_md_read (md, + GCRY_MD_SHA1); + GNUNET_assert (NULL != mc); + memcpy (hmac, + mc, + sizeof (hmac)); + gcry_md_close (md); + } + + { + uint32_t code = 0; + + for (int count = 0; count < 4; count++) + code += hmac[(hmac[sizeof (hmac) - 1] & 0x0f) + 3 - count] << 8 * count; + code &= 0x7fffffff; + +#if VAR_DIGITS + if (digits == 6) + code = code % 1000000; + else if (digits == 7) + code = code % 10000000; + else if (digits == 8) + code = code % 100000000; +#else + code = code % 1000000; +#endif + return code; + } +} + + +/** + * Begin issuing authentication challenge to user based on @a data. + * + * @param cls closure + * @param trigger function to call when we made progress + * @param trigger_cls closure for @a trigger + * @param truth_uuid Identifier of the challenge, to be (if possible) included in the + * interaction with the user + * @param code set to secret code that the user provided to satisfy the challenge in + * the main anastasis protocol + * @param data input to validate (i.e. the shared secret) + * @param data_length number of bytes in @a data + * @return state to track progress on the authorization operation, NULL on failure + */ +static struct ANASTASIS_AUTHORIZATION_State * +totp_start (void *cls, + GNUNET_SCHEDULER_TaskCallback trigger, + void *trigger_cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + uint64_t code, + const void *data, + size_t data_length) +{ + const struct ANASTASIS_AuthorizationContext *ac = cls; + struct ANASTASIS_AUTHORIZATION_State *as; + uint64_t want; + + as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); + as->ac = ac; + as->truth_uuid = *truth_uuid; + for (int i = -TIME_INTERVAL_RANGE; + i < TIME_INTERVAL_RANGE; + i++) + { + want = compute_totp (i, + data, + data_length); + if (code == want) + as->ok = true; + } + return as; +} + + +/** + * Begin issuing authentication challenge to user based on @a data. + * + * @param as authorization state + * @param timeout how long do we have to produce a reply + * @param connection HTTP client request (for queuing response, such as redirection to video portal) + * @return state of the request + */ +static enum ANASTASIS_AUTHORIZATION_Result +totp_process (struct ANASTASIS_AUTHORIZATION_State *as, + struct GNUNET_TIME_Absolute timeout, + struct MHD_Connection *connection) +{ + MHD_RESULT mres; + const char *mime; + const char *lang; + + if (as->ok) + return ANASTASIS_AUTHORIZATION_RES_FINISHED; + mime = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT); + if (NULL == mime) + mime = "text/plain"; + lang = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == lang) + lang = "en"; + + /* Build HTTP response */ + { + struct MHD_Response *resp; + struct GNUNET_TIME_Absolute now; + + now = GNUNET_TIME_absolute_get (); + if (TALER_MHD_xmime_matches (mime, + "application/json")) + { + resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_time_abs ("server_time", + now)); + } + else + { + size_t response_size; + char *response; + + // FIXME: i18n of the message based on 'lang' ... + response_size + = GNUNET_asprintf (&response, + "Server time: %s", + GNUNET_STRINGS_absolute_time_to_string (now)); + resp = MHD_create_response_from_buffer (response_size, + response, + MHD_RESPMEM_MUST_COPY); + TALER_MHD_add_global_headers (resp); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_CONTENT_TYPE, + "text/plain")); + } + mres = MHD_queue_response (connection, + MHD_HTTP_FORBIDDEN, + resp); + MHD_destroy_response (resp); + } + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_SUCCESS_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_SUCCESS; +} + + +/** + * Free internal state associated with @a as. + * + * @param as state to clean up + */ +static void +totp_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) +{ + GNUNET_free (as); +} + + +/** + * Initialize Totp based authorization plugin + * + * @param cls a configuration instance + * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` + */ +void * +libanastasis_plugin_authorization_totp_init (void *cls) +{ + const struct ANASTASIS_AuthorizationContext *ac = cls; + struct ANASTASIS_AuthorizationPlugin *plugin; + + plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); + plugin->cls = (void *) ac; + plugin->user_provided_code = true; + plugin->retry_counter = INITIAL_RETRY_COUNTER; + plugin->code_validity_period = TOTP_VALIDITY_PERIOD; + plugin->code_rotation_period = plugin->code_validity_period; + plugin->code_retransmission_frequency = plugin->code_validity_period; + plugin->validate = &totp_validate; + plugin->start = &totp_start; + plugin->process = &totp_process; + plugin->cleanup = &totp_cleanup; + return plugin; +} + + +/** + * Unload authorization plugin + * + * @param cls a `struct ANASTASIS_AuthorizationPlugin` + * @return NULL (always) + */ +void * +libanastasis_plugin_authorization_totp_done (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin = cls; + + GNUNET_free (plugin); + return NULL; +} diff --git a/src/include/anastasis_authorization_plugin.h b/src/include/anastasis_authorization_plugin.h index 91a88f8..48d8741 100644 --- a/src/include/anastasis_authorization_plugin.h +++ b/src/include/anastasis_authorization_plugin.h @@ -126,6 +126,14 @@ struct ANASTASIS_AuthorizationPlugin */ bool payment_plugin_managed; + /** + * The plugin expects the "code" in the "start" function to be + * provided by the user and not generated by the Anastasis + * backend. The plugin will then validate the code using its own + * means. Used by TOTP. + */ + bool user_provided_code; + /** * How often are retries allowed for challenges created * by this plugin? -- cgit v1.2.3