anastasis

Credential backup and recovery protocol and service
Log | Files | Refs | Submodules | README | LICENSE

anastasis_authorization_plugin_totp.c (11764B)


      1 /*
      2   This totp is part of Anastasis
      3   Copyright (C) 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 totp COPYING.GPL.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @totp anastasis_authorization_plugin_totp.c
     18  * @brief authorization plugin using totp
     19  * @author Christian Grothoff
     20  */
     21 #include "platform.h"
     22 #include "anastasis_authorization_plugin.h"
     23 #include <taler/taler_mhd_lib.h>
     24 #include <gnunet/gnunet_db_lib.h>
     25 #include "anastasis_database_lib.h"
     26 #include <gcrypt.h>
     27 
     28 
     29 /**
     30  * How many retries do we allow per code?
     31  */
     32 #define INITIAL_RETRY_COUNTER 3
     33 
     34 /**
     35  * How long is a TOTP code valid?
     36  */
     37 #define TOTP_VALIDITY_PERIOD GNUNET_TIME_relative_multiply ( \
     38           GNUNET_TIME_UNIT_SECONDS, 30)
     39 
     40 /**
     41  * Range of time we allow (plus-minus).
     42  */
     43 #define TIME_INTERVAL_RANGE 2
     44 
     45 /**
     46  * How long is the shared secret in bytes?
     47  */
     48 #define SECRET_LEN 32
     49 
     50 
     51 /**
     52  * Saves the state of a authorization process
     53  */
     54 struct ANASTASIS_AUTHORIZATION_State
     55 {
     56   /**
     57    * UUID of the challenge which is authorised
     58    */
     59   struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid;
     60 
     61   /**
     62    * Was the challenge satisfied?
     63    */
     64   struct GNUNET_HashCode valid_replies[TIME_INTERVAL_RANGE * 2 + 1];
     65 
     66   /**
     67    * Our context.
     68    */
     69   const struct ANASTASIS_AuthorizationContext *ac;
     70 
     71 };
     72 
     73 
     74 /**
     75  * Validate @a data is a well-formed input into the challenge method,
     76  * i.e. @a data is a well-formed phone number for sending an SMS, or
     77  * a well-formed e-mail address for sending an e-mail. Not expected to
     78  * check that the phone number or e-mail account actually exists.
     79  *
     80  * To be possibly used before issuing a 402 payment required to the client.
     81  *
     82  * @param cls closure with a `const struct ANASTASIS_AuthorizationContext *`
     83  * @param connection HTTP client request (for queuing response)
     84  * @param truth_mime mime type of @e data
     85  * @param data input to validate (i.e. is it a valid phone number, etc.)
     86  * @param data_length number of bytes in @a data
     87  * @return #GNUNET_OK if @a data is valid,
     88  *         #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection
     89  *         #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection
     90  */
     91 static enum GNUNET_GenericReturnValue
     92 totp_validate (void *cls,
     93                struct MHD_Connection *connection,
     94                const char *truth_mime,
     95                const char *data,
     96                size_t data_length)
     97 {
     98   (void) cls;
     99   (void) truth_mime;
    100   (void) connection;
    101   if (NULL == data)
    102   {
    103     GNUNET_break_op (0);
    104     if (MHD_NO ==
    105         TALER_MHD_reply_with_error (connection,
    106                                     MHD_HTTP_CONFLICT,
    107                                     TALER_EC_ANASTASIS_TOTP_KEY_MISSING,
    108                                     NULL))
    109       return GNUNET_SYSERR;
    110     return GNUNET_NO;
    111   }
    112   if (SECRET_LEN != data_length)
    113   {
    114     GNUNET_break_op (0);
    115     if (MHD_NO ==
    116         TALER_MHD_reply_with_error (connection,
    117                                     MHD_HTTP_CONFLICT,
    118                                     TALER_EC_ANASTASIS_TOTP_KEY_INVALID,
    119                                     NULL))
    120       return GNUNET_SYSERR;
    121     return GNUNET_NO;
    122   }
    123   return GNUNET_OK;
    124 }
    125 
    126 
    127 /**
    128  * Compute TOTP code at current time with offset
    129  * @a time_off for the @a key.
    130  *
    131  * @param time_off offset to apply when computing the code
    132  * @param key input key material
    133  * @param key_size number of bytes in @a key
    134  * @return TOTP code at this time
    135  */
    136 static uint64_t
    137 compute_totp (int time_off,
    138               const void *key,
    139               size_t key_size)
    140 {
    141   struct GNUNET_TIME_Absolute now;
    142   time_t t;
    143   uint64_t ctr;
    144   uint8_t hmac[20]; /* SHA1: 20 bytes */
    145 
    146   now = GNUNET_TIME_absolute_get ();
    147   while (time_off < 0)
    148   {
    149     now = GNUNET_TIME_absolute_subtract (now,
    150                                          TOTP_VALIDITY_PERIOD);
    151     time_off++;
    152   }
    153   while (time_off > 0)
    154   {
    155     now = GNUNET_TIME_absolute_add (now,
    156                                     TOTP_VALIDITY_PERIOD);
    157     time_off--;
    158   }
    159   t = now.abs_value_us / GNUNET_TIME_UNIT_SECONDS.rel_value_us;
    160   ctr = GNUNET_htonll (t / 30LLU);
    161 
    162   {
    163     gcry_md_hd_t md;
    164     const unsigned char *mc;
    165 
    166     GNUNET_assert (GPG_ERR_NO_ERROR ==
    167                    gcry_md_open (&md,
    168                                  GCRY_MD_SHA1,
    169                                  GCRY_MD_FLAG_HMAC));
    170     GNUNET_assert (GPG_ERR_NO_ERROR ==
    171                    gcry_md_setkey (md,
    172                                    key,
    173                                    key_size));
    174     gcry_md_write (md,
    175                    &ctr,
    176                    sizeof (ctr));
    177     mc = gcry_md_read (md,
    178                        GCRY_MD_SHA1);
    179     GNUNET_assert (NULL != mc);
    180     memcpy (hmac,
    181             mc,
    182             sizeof (hmac));
    183     gcry_md_close (md);
    184   }
    185 
    186   {
    187     uint32_t code = 0;
    188     int offset;
    189 
    190     offset = hmac[sizeof (hmac) - 1] & 0x0f;
    191     for (int count = 0; count < 4; count++)
    192       code |= ((uint32_t) hmac[offset + 3 - count]) << (8 * count);
    193     code &= 0x7fffffff;
    194     /* always use 8 digits (maximum) */
    195     code = code % 100000000;
    196     return code;
    197   }
    198 }
    199 
    200 
    201 /**
    202  * Begin issuing authentication challenge to user based on @a data.
    203  *
    204  * @param cls closure
    205  * @param trigger function to call when we made progress
    206  * @param trigger_cls closure for @a trigger
    207  * @param truth_uuid Identifier of the challenge, to be (if possible) included in the
    208  *             interaction with the user
    209  * @param code always 0 (direct validation, backend does
    210  *             not generate a code in this mode)
    211  * @param data truth for input to validate (i.e. the shared secret)
    212  * @param data_length number of bytes in @a data
    213  * @return state to track progress on the authorization operation, NULL on failure
    214  */
    215 static struct ANASTASIS_AUTHORIZATION_State *
    216 totp_start (void *cls,
    217             GNUNET_SCHEDULER_TaskCallback trigger,
    218             void *trigger_cls,
    219             const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
    220             uint64_t code,
    221             const void *data,
    222             size_t data_length)
    223 {
    224   const struct ANASTASIS_AuthorizationContext *ac = cls;
    225   struct ANASTASIS_AUTHORIZATION_State *as;
    226   uint64_t want;
    227   unsigned int off = 0;
    228 
    229   GNUNET_assert (0 == code);
    230   as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State);
    231   as->ac = ac;
    232   as->truth_uuid = *truth_uuid;
    233   for (int i = -TIME_INTERVAL_RANGE;
    234        i <= TIME_INTERVAL_RANGE;
    235        i++)
    236   {
    237     want = compute_totp (i,
    238                          data,
    239                          data_length);
    240     ANASTASIS_hash_answer (want,
    241                            &as->valid_replies[off++]);
    242   }
    243   return as;
    244 }
    245 
    246 
    247 /**
    248  * Check authentication response from the user.
    249  *
    250  * @param as authorization state
    251  * @param timeout how long do we have to produce a reply
    252  * @param challenge_response hash of the response
    253  * @param connection HTTP client request (for queuing response, such as redirection to video portal)
    254  * @return state of the request
    255  */
    256 static enum ANASTASIS_AUTHORIZATION_SolveResult
    257 totp_solve (struct ANASTASIS_AUTHORIZATION_State *as,
    258             struct GNUNET_TIME_Absolute timeout,
    259             const struct GNUNET_HashCode *challenge_response,
    260             struct MHD_Connection *connection)
    261 {
    262   MHD_RESULT mres;
    263   const char *mime;
    264   const char *lang;
    265 
    266   for (unsigned int i = 0; i<=TIME_INTERVAL_RANGE * 2; i++)
    267     if (0 ==
    268         GNUNET_memcmp (challenge_response,
    269                        &as->valid_replies[i]))
    270       return ANASTASIS_AUTHORIZATION_SRES_FINISHED;
    271   mime = MHD_lookup_connection_value (connection,
    272                                       MHD_HEADER_KIND,
    273                                       MHD_HTTP_HEADER_ACCEPT);
    274   if (NULL == mime)
    275     mime = "text/plain";
    276   lang = MHD_lookup_connection_value (connection,
    277                                       MHD_HEADER_KIND,
    278                                       MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
    279   if (NULL == lang)
    280     lang = "en";
    281 
    282   /* Build HTTP response */
    283   {
    284     struct MHD_Response *resp;
    285     struct GNUNET_TIME_Timestamp now;
    286 
    287     now = GNUNET_TIME_timestamp_get ();
    288     if (0.0 < TALER_pattern_matches (mime,
    289                                      "application/json"))
    290     {
    291       resp = TALER_MHD_MAKE_JSON_PACK (
    292         GNUNET_JSON_pack_uint64 ("code",
    293                                  TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED),
    294         GNUNET_JSON_pack_string ("hint",
    295                                  TALER_ErrorCode_get_hint (
    296                                    TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED)),
    297         GNUNET_JSON_pack_timestamp ("server_time",
    298                                     now));
    299     }
    300     else
    301     {
    302       size_t response_size;
    303       char *response;
    304 
    305       // FIXME: i18n of the message based on 'lang' ...
    306       response_size
    307         = GNUNET_asprintf (&response,
    308                            "Server time: %s",
    309                            GNUNET_TIME_timestamp2s (now));
    310       resp = MHD_create_response_from_buffer (response_size,
    311                                               response,
    312                                               MHD_RESPMEM_MUST_COPY);
    313       TALER_MHD_add_global_headers (resp,
    314                                     false);
    315       GNUNET_break (MHD_YES ==
    316                     MHD_add_response_header (resp,
    317                                              MHD_HTTP_HEADER_CONTENT_TYPE,
    318                                              "text/plain"));
    319     }
    320     mres = MHD_queue_response (connection,
    321                                MHD_HTTP_FORBIDDEN,
    322                                resp);
    323     MHD_destroy_response (resp);
    324   }
    325   if (MHD_YES != mres)
    326     return ANASTASIS_AUTHORIZATION_SRES_FAILED_REPLY_FAILED;
    327   return ANASTASIS_AUTHORIZATION_SRES_FAILED;
    328 }
    329 
    330 
    331 /**
    332  * Free internal state associated with @a as.
    333  *
    334  * @param as state to clean up
    335  */
    336 static void
    337 totp_cleanup (struct ANASTASIS_AUTHORIZATION_State *as)
    338 {
    339   GNUNET_free (as);
    340 }
    341 
    342 
    343 /**
    344  * Initialize Totp based authorization plugin
    345  *
    346  * @param cls a configuration instance
    347  * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin`
    348  */
    349 void *
    350 libanastasis_plugin_authorization_totp_init (void *cls);
    351 
    352 /* declaration to fix compiler warning */
    353 void *
    354 libanastasis_plugin_authorization_totp_init (void *cls)
    355 {
    356   const struct ANASTASIS_AuthorizationContext *ac = cls;
    357   struct ANASTASIS_AuthorizationPlugin *plugin;
    358 
    359   plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin);
    360   plugin->cls = (void *) ac;
    361   plugin->user_provided_code = true;
    362   plugin->retry_counter = INITIAL_RETRY_COUNTER;
    363   plugin->code_validity_period = TOTP_VALIDITY_PERIOD;
    364   plugin->code_rotation_period = plugin->code_validity_period;
    365   plugin->code_retransmission_frequency = plugin->code_validity_period;
    366   plugin->validate = &totp_validate;
    367   plugin->start = &totp_start;
    368   plugin->solve = &totp_solve;
    369   plugin->cleanup = &totp_cleanup;
    370   return plugin;
    371 }
    372 
    373 
    374 /**
    375  * Unload authorization plugin
    376  *
    377  * @param cls a `struct ANASTASIS_AuthorizationPlugin`
    378  * @return NULL (always)
    379  */
    380 void *
    381 libanastasis_plugin_authorization_totp_done (void *cls);
    382 
    383 /* declaration to fix compiler warning */
    384 void *
    385 libanastasis_plugin_authorization_totp_done (void *cls)
    386 {
    387   struct ANASTASIS_AuthorizationPlugin *plugin = cls;
    388 
    389   GNUNET_free (plugin);
    390   return NULL;
    391 }