anastasis

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

anastasis_authorization_plugin_totp.c (13879B)


      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    * Time step counter each entry of @e valid_replies was computed for,
     68    * in the same order.  Needed to tell a replay from a fresh code.
     69    */
     70   uint64_t counters[TIME_INTERVAL_RANGE * 2 + 1];
     71 
     72   /**
     73    * Our context.
     74    */
     75   const struct ANASTASIS_AuthorizationContext *ac;
     76 
     77 };
     78 
     79 
     80 /**
     81  * Validate @a data is a well-formed input into the challenge method,
     82  * i.e. @a data is a well-formed phone number for sending an SMS, or
     83  * a well-formed e-mail address for sending an e-mail. Not expected to
     84  * check that the phone number or e-mail account actually exists.
     85  *
     86  * To be possibly used before issuing a 402 payment required to the client.
     87  *
     88  * @param cls closure with a `const struct ANASTASIS_AuthorizationContext *`
     89  * @param connection HTTP client request (for queuing response)
     90  * @param truth_mime mime type of @e data
     91  * @param data input to validate (i.e. is it a valid phone number, etc.)
     92  * @param data_length number of bytes in @a data
     93  * @return #GNUNET_OK if @a data is valid,
     94  *         #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection
     95  *         #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection
     96  */
     97 static enum GNUNET_GenericReturnValue
     98 totp_validate (void *cls,
     99                struct MHD_Connection *connection,
    100                const char *truth_mime,
    101                const char *data,
    102                size_t data_length)
    103 {
    104   (void) cls;
    105   (void) truth_mime;
    106   (void) connection;
    107   if (NULL == data)
    108   {
    109     GNUNET_break_op (0);
    110     if (MHD_NO ==
    111         TALER_MHD_reply_with_error (connection,
    112                                     MHD_HTTP_CONFLICT,
    113                                     TALER_EC_ANASTASIS_TOTP_KEY_MISSING,
    114                                     NULL))
    115       return GNUNET_SYSERR;
    116     return GNUNET_NO;
    117   }
    118   if (SECRET_LEN != data_length)
    119   {
    120     GNUNET_break_op (0);
    121     if (MHD_NO ==
    122         TALER_MHD_reply_with_error (connection,
    123                                     MHD_HTTP_CONFLICT,
    124                                     TALER_EC_ANASTASIS_TOTP_KEY_INVALID,
    125                                     NULL))
    126       return GNUNET_SYSERR;
    127     return GNUNET_NO;
    128   }
    129   return GNUNET_OK;
    130 }
    131 
    132 
    133 /**
    134  * Compute TOTP code at current time with offset
    135  * @a time_off for the @a key.
    136  *
    137  * @param time_off offset to apply when computing the code
    138  * @param key input key material
    139  * @param key_size number of bytes in @a key
    140  * @param[out] counter set to the time step counter the code belongs to
    141  * @return TOTP code at this time
    142  */
    143 static uint64_t
    144 compute_totp (int time_off,
    145               const void *key,
    146               size_t key_size,
    147               uint64_t *counter)
    148 {
    149   struct GNUNET_TIME_Absolute now;
    150   time_t t;
    151   uint64_t ctr;
    152   uint8_t hmac[20]; /* SHA1: 20 bytes */
    153 
    154   now = GNUNET_TIME_absolute_get ();
    155   while (time_off < 0)
    156   {
    157     now = GNUNET_TIME_absolute_subtract (now,
    158                                          TOTP_VALIDITY_PERIOD);
    159     time_off++;
    160   }
    161   while (time_off > 0)
    162   {
    163     now = GNUNET_TIME_absolute_add (now,
    164                                     TOTP_VALIDITY_PERIOD);
    165     time_off--;
    166   }
    167   t = now.abs_value_us / GNUNET_TIME_UNIT_SECONDS.rel_value_us;
    168   *counter = t / 30LLU;
    169   ctr = GNUNET_htonll (*counter);
    170 
    171   {
    172     gcry_md_hd_t md;
    173     const unsigned char *mc;
    174 
    175     GNUNET_assert (GPG_ERR_NO_ERROR ==
    176                    gcry_md_open (&md,
    177                                  GCRY_MD_SHA1,
    178                                  GCRY_MD_FLAG_HMAC));
    179     GNUNET_assert (GPG_ERR_NO_ERROR ==
    180                    gcry_md_setkey (md,
    181                                    key,
    182                                    key_size));
    183     gcry_md_write (md,
    184                    &ctr,
    185                    sizeof (ctr));
    186     mc = gcry_md_read (md,
    187                        GCRY_MD_SHA1);
    188     GNUNET_assert (NULL != mc);
    189     memcpy (hmac,
    190             mc,
    191             sizeof (hmac));
    192     gcry_md_close (md);
    193   }
    194 
    195   {
    196     uint32_t code = 0;
    197     int offset;
    198 
    199     offset = hmac[sizeof (hmac) - 1] & 0x0f;
    200     for (int count = 0; count < 4; count++)
    201       code |= ((uint32_t) hmac[offset + 3 - count]) << (8 * count);
    202     code &= 0x7fffffff;
    203     /* always use 8 digits (maximum) */
    204     code = code % 100000000;
    205     return code;
    206   }
    207 }
    208 
    209 
    210 /**
    211  * Begin issuing authentication challenge to user based on @a data.
    212  *
    213  * @param cls closure
    214  * @param trigger function to call when we made progress
    215  * @param trigger_cls closure for @a trigger
    216  * @param truth_uuid Identifier of the challenge, to be (if possible) included in the
    217  *             interaction with the user
    218  * @param code always 0 (direct validation, backend does
    219  *             not generate a code in this mode)
    220  * @param data truth for input to validate (i.e. the shared secret)
    221  * @param data_length number of bytes in @a data
    222  * @return state to track progress on the authorization operation, NULL on failure
    223  */
    224 static struct ANASTASIS_AUTHORIZATION_State *
    225 totp_start (void *cls,
    226             GNUNET_SCHEDULER_TaskCallback trigger,
    227             void *trigger_cls,
    228             const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
    229             uint64_t code,
    230             const void *data,
    231             size_t data_length)
    232 {
    233   const struct ANASTASIS_AuthorizationContext *ac = cls;
    234   struct ANASTASIS_AUTHORIZATION_State *as;
    235   uint64_t want;
    236   unsigned int off = 0;
    237 
    238   GNUNET_assert (0 == code);
    239   if (SECRET_LEN != data_length)
    240   {
    241     /* totp_validate() enforces this on upload, but the truth we are handed
    242        here comes back from the database, so do not trust its length. */
    243     GNUNET_break_op (0);
    244     return NULL;
    245   }
    246   as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State);
    247   as->ac = ac;
    248   as->truth_uuid = *truth_uuid;
    249   for (int i = -TIME_INTERVAL_RANGE;
    250        i <= TIME_INTERVAL_RANGE;
    251        i++)
    252   {
    253     want = compute_totp (i,
    254                          data,
    255                          data_length,
    256                          &as->counters[off]);
    257     ANASTASIS_hash_answer (want,
    258                            &as->valid_replies[off++]);
    259   }
    260   return as;
    261 }
    262 
    263 
    264 /**
    265  * Check authentication response from the user.
    266  *
    267  * @param as authorization state
    268  * @param timeout how long do we have to produce a reply
    269  * @param challenge_response hash of the response
    270  * @param connection HTTP client request (for queuing response, such as redirection to video portal)
    271  * @return state of the request
    272  */
    273 static enum ANASTASIS_AUTHORIZATION_SolveResult
    274 totp_solve (struct ANASTASIS_AUTHORIZATION_State *as,
    275             struct GNUNET_TIME_Absolute timeout,
    276             const struct GNUNET_HashCode *challenge_response,
    277             struct MHD_Connection *connection)
    278 {
    279   enum MHD_Result mres;
    280   const char *mime;
    281   const char *lang;
    282 
    283   for (unsigned int i = 0; i<=TIME_INTERVAL_RANGE * 2; i++)
    284   {
    285     enum GNUNET_DB_QueryStatus qs;
    286 
    287     if (0 !=
    288         GNUNET_memcmp (challenge_response,
    289                        &as->valid_replies[i]))
    290       continue;
    291     /* The code is right for time step @e counters[i], but a code stays right
    292        for as long as its window lasts, so anyone who saw it could send it
    293        again.  Consume the time step: this succeeds at most once (RFC 6238,
    294        section 5.2), and because the check and the store are the same
    295        statement two requests carrying the same code cannot both win. */
    296     qs = ANASTASIS_DB_update_totp_counter (&as->truth_uuid,
    297                                            as->counters[i]);
    298     if (qs < 0)
    299     {
    300       GNUNET_break (0);
    301       if (MHD_NO ==
    302           TALER_MHD_reply_with_error (connection,
    303                                       MHD_HTTP_INTERNAL_SERVER_ERROR,
    304                                       TALER_EC_GENERIC_DB_STORE_FAILED,
    305                                       "update_totp_counter"))
    306         return ANASTASIS_AUTHORIZATION_SRES_FAILED_REPLY_FAILED;
    307       return ANASTASIS_AUTHORIZATION_SRES_FAILED;
    308     }
    309     if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
    310     {
    311       /* Replay of a code we already honoured (or the truth is gone by now).
    312          Answer exactly as for a wrong code, so that the client learns
    313          nothing from which of the two it was. */
    314       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    315                   "Refusing TOTP code for time step %llu of truth %s: already used\n",
    316                   (unsigned long long) as->counters[i],
    317                   TALER_B2S (&as->truth_uuid));
    318       break;
    319     }
    320     return ANASTASIS_AUTHORIZATION_SRES_FINISHED;
    321   }
    322   mime = MHD_lookup_connection_value (connection,
    323                                       MHD_HEADER_KIND,
    324                                       MHD_HTTP_HEADER_ACCEPT);
    325   if (NULL == mime)
    326     mime = "text/plain";
    327   lang = MHD_lookup_connection_value (connection,
    328                                       MHD_HEADER_KIND,
    329                                       MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
    330   if (NULL == lang)
    331     lang = "en";
    332 
    333   /* Build HTTP response */
    334   {
    335     struct MHD_Response *resp;
    336     struct GNUNET_TIME_Timestamp now;
    337 
    338     now = GNUNET_TIME_timestamp_get ();
    339     if (0.0 < TALER_pattern_matches (mime,
    340                                      "application/json"))
    341     {
    342       resp = TALER_MHD_MAKE_JSON_PACK (
    343         GNUNET_JSON_pack_uint64 ("code",
    344                                  TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED),
    345         GNUNET_JSON_pack_string ("hint",
    346                                  TALER_ErrorCode_get_hint (
    347                                    TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED)),
    348         GNUNET_JSON_pack_timestamp ("server_time",
    349                                     now));
    350     }
    351     else
    352     {
    353       size_t response_size;
    354       char *response;
    355 
    356       // FIXME: i18n of the message based on 'lang' ...
    357       response_size
    358         = GNUNET_asprintf (&response,
    359                            "Server time: %s",
    360                            GNUNET_TIME_timestamp2s (now));
    361       resp = MHD_create_response_from_buffer (response_size,
    362                                               response,
    363                                               MHD_RESPMEM_MUST_COPY);
    364       TALER_MHD_add_global_headers (resp,
    365                                     false);
    366       GNUNET_break (MHD_YES ==
    367                     MHD_add_response_header (resp,
    368                                              MHD_HTTP_HEADER_CONTENT_TYPE,
    369                                              "text/plain"));
    370     }
    371     mres = MHD_queue_response (connection,
    372                                MHD_HTTP_FORBIDDEN,
    373                                resp);
    374     MHD_destroy_response (resp);
    375   }
    376   if (MHD_YES != mres)
    377     return ANASTASIS_AUTHORIZATION_SRES_FAILED_REPLY_FAILED;
    378   return ANASTASIS_AUTHORIZATION_SRES_FAILED;
    379 }
    380 
    381 
    382 /**
    383  * Free internal state associated with @a as.
    384  *
    385  * @param as state to clean up
    386  */
    387 static void
    388 totp_cleanup (struct ANASTASIS_AUTHORIZATION_State *as)
    389 {
    390   GNUNET_free (as);
    391 }
    392 
    393 
    394 /**
    395  * Initialize Totp based authorization plugin
    396  *
    397  * @param cls a configuration instance
    398  * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin`
    399  */
    400 void *
    401 libanastasis_plugin_authorization_totp_init (void *cls);
    402 
    403 /* declaration to fix compiler warning */
    404 void *
    405 libanastasis_plugin_authorization_totp_init (void *cls)
    406 {
    407   const struct ANASTASIS_AuthorizationContext *ac = cls;
    408   struct ANASTASIS_AuthorizationPlugin *plugin;
    409 
    410   plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin);
    411   plugin->cls = (void *) ac;
    412   plugin->user_provided_code = true;
    413   plugin->retry_counter = INITIAL_RETRY_COUNTER;
    414   plugin->code_validity_period = TOTP_VALIDITY_PERIOD;
    415   plugin->code_rotation_period = plugin->code_validity_period;
    416   plugin->code_retransmission_frequency = plugin->code_validity_period;
    417   plugin->validate = &totp_validate;
    418   plugin->start = &totp_start;
    419   plugin->solve = &totp_solve;
    420   plugin->cleanup = &totp_cleanup;
    421   return plugin;
    422 }
    423 
    424 
    425 /**
    426  * Unload authorization plugin
    427  *
    428  * @param cls a `struct ANASTASIS_AuthorizationPlugin`
    429  * @return NULL (always)
    430  */
    431 void *
    432 libanastasis_plugin_authorization_totp_done (void *cls);
    433 
    434 /* declaration to fix compiler warning */
    435 void *
    436 libanastasis_plugin_authorization_totp_done (void *cls)
    437 {
    438   struct ANASTASIS_AuthorizationPlugin *plugin = cls;
    439 
    440   GNUNET_free (plugin);
    441   return NULL;
    442 }