taler-merchant-httpd_post-private-otp-devices.c (11093B)
1 /* 2 This file is part of TALER 3 (C) 2022 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify 6 it under the terms of the GNU Affero General Public License as 7 published by the Free Software Foundation; either version 3, 8 or (at your option) any later version. 9 10 TALER is distributed in the hope that it will be useful, but 11 WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 GNU General Public License for more details. 14 15 You should have received a copy of the GNU General Public 16 License along with TALER; see the file COPYING. If not, 17 see <http://www.gnu.org/licenses/> 18 */ 19 20 /** 21 * @file src/backend/taler-merchant-httpd_post-private-otp-devices.c 22 * @brief implementing POST /otp-devices request handling 23 * @author Christian Grothoff 24 */ 25 #include "platform.h" 26 #include "taler-merchant-httpd_post-private-otp-devices.h" 27 #include "taler-merchant-httpd_helper.h" 28 #include <taler/taler_json_lib.h> 29 #include "merchant-database/insert_otp_device.h" 30 #include "merchant-database/get_otp_device.h" 31 #include "merchant-database/start.h" 32 33 34 /** 35 * How often do we retry the simple INSERT database transaction? 36 */ 37 #define MAX_RETRIES 3 38 39 40 /** 41 * Check if the two otp-devices are identical. 42 * 43 * @param t1 device to compare 44 * @param t2 other device to compare 45 * @return true if they are 'equal', false if not or of payto_uris is not an array 46 */ 47 static bool 48 otp_devices_equal (const struct TALER_MERCHANTDB_OtpDeviceDetails *t1, 49 const struct TALER_MERCHANTDB_OtpDeviceDetails *t2) 50 { 51 if ( (0 != strcmp (t1->otp_description, 52 t2->otp_description)) || 53 (t1->otp_ctr != t2->otp_ctr) || 54 (t1->otp_algorithm != t2->otp_algorithm) ) 55 return false; 56 switch (t1->otp_algorithm) 57 { 58 case TALER_MCA_ECDSA_CHALLENGE: 59 case TALER_MCA_EDDSA_CHALLENGE: 60 /* the key was generated here and never shown to the client, so 61 there is nothing of theirs left to compare */ 62 return true; 63 case TALER_MCA_NONE: 64 case TALER_MCA_WITHOUT_PRICE: 65 case TALER_MCA_WITH_PRICE: 66 return ( (NULL == t1->otp_key) == (NULL == t2->otp_key) ) && 67 ( (NULL == t1->otp_key) || 68 (0 == strcmp (t1->otp_key, 69 t2->otp_key)) ); 70 } 71 GNUNET_break (0); 72 return false; 73 } 74 75 76 enum MHD_Result 77 TMH_private_post_otp_devices (const struct TMH_RequestHandler *rh, 78 struct MHD_Connection *connection, 79 struct TMH_HandlerContext *hc) 80 { 81 struct TMH_MerchantInstance *mi = hc->instance; 82 struct TALER_MERCHANTDB_OtpDeviceDetails tp = { 0 }; 83 const char *device_id; 84 enum GNUNET_DB_QueryStatus qs; 85 enum MHD_Result ret; 86 bool generated_keys = false; 87 struct GNUNET_JSON_Specification spec[] = { 88 TALER_JSON_spec_slug ("otp_device_id", 89 &device_id), 90 GNUNET_JSON_spec_string ("otp_device_description", 91 (const char **) &tp.otp_description), 92 TALER_JSON_spec_otp_type ("otp_algorithm", 93 &tp.otp_algorithm), 94 GNUNET_JSON_spec_mark_optional ( 95 GNUNET_JSON_spec_uint64 ("otp_ctr", 96 &tp.otp_ctr), 97 NULL), 98 GNUNET_JSON_spec_mark_optional ( 99 TALER_JSON_spec_otp_key ("otp_key", 100 (const char **) &tp.otp_key), 101 NULL), 102 GNUNET_JSON_spec_end () 103 }; 104 105 GNUNET_assert (NULL != mi); 106 { 107 enum GNUNET_GenericReturnValue res; 108 109 res = TALER_MHD_parse_json_data (connection, 110 hc->request_body, 111 spec); 112 if (GNUNET_OK != res) 113 { 114 GNUNET_break_op (0); 115 return (GNUNET_NO == res) 116 ? MHD_YES 117 : MHD_NO; 118 } 119 } 120 /* returned to (JavaScript) clients as a JSON number, so it must be 121 a "SafeUint64" (2^53 - 1), see api-common.rst */ 122 if (tp.otp_ctr > (1LLU << 53) - 1) 123 { 124 GNUNET_break_op (0); 125 ret = TALER_MHD_reply_with_error (connection, 126 MHD_HTTP_BAD_REQUEST, 127 TALER_EC_GENERIC_PARAMETER_MALFORMED, 128 "otp_ctr"); 129 goto cleanup; 130 } 131 132 switch (tp.otp_algorithm) 133 { 134 case TALER_MCA_ECDSA_CHALLENGE: 135 case TALER_MCA_EDDSA_CHALLENGE: 136 if (NULL != tp.otp_key) 137 { 138 GNUNET_break_op (0); 139 ret = TALER_MHD_reply_with_error (connection, 140 MHD_HTTP_BAD_REQUEST, 141 TALER_EC_GENERIC_PARAMETER_MALFORMED, 142 "otp_key"); 143 goto cleanup; 144 } 145 break; 146 case TALER_MCA_NONE: 147 case TALER_MCA_WITHOUT_PRICE: 148 case TALER_MCA_WITH_PRICE: 149 /* the merchant shares these with the OTP application itself */ 150 if (NULL == tp.otp_key) 151 { 152 GNUNET_break_op (0); 153 ret = TALER_MHD_reply_with_error (connection, 154 MHD_HTTP_BAD_REQUEST, 155 TALER_EC_GENERIC_PARAMETER_MISSING, 156 "otp_key"); 157 goto cleanup; 158 } 159 break; 160 } 161 162 /* finally, interact with DB until no serialization error */ 163 for (unsigned int i = 0; i<MAX_RETRIES; i++) 164 { 165 /* Test if a OTP device of this id is known */ 166 struct TALER_MERCHANTDB_OtpDeviceDetails etp; 167 168 if (GNUNET_OK != 169 TALER_MERCHANTDB_start (TMH_db, 170 "/post otp-devices")) 171 { 172 GNUNET_break (0); 173 ret = TALER_MHD_reply_with_error (connection, 174 MHD_HTTP_INTERNAL_SERVER_ERROR, 175 TALER_EC_GENERIC_DB_START_FAILED, 176 NULL); 177 goto cleanup; 178 } 179 qs = TALER_MERCHANTDB_get_otp_device (TMH_db, 180 mi->settings.id, 181 device_id, 182 &etp); 183 switch (qs) 184 { 185 case GNUNET_DB_STATUS_HARD_ERROR: 186 /* Clean up and fail hard */ 187 GNUNET_break (0); 188 TALER_MERCHANTDB_rollback (TMH_db); 189 ret = TALER_MHD_reply_with_error (connection, 190 MHD_HTTP_INTERNAL_SERVER_ERROR, 191 TALER_EC_GENERIC_DB_FETCH_FAILED, 192 NULL); 193 goto cleanup; 194 case GNUNET_DB_STATUS_SOFT_ERROR: 195 /* restart transaction */ 196 goto retry; 197 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 198 /* Good, we can proceed! */ 199 break; 200 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 201 /* idempotency check: is etp == tp? */ 202 { 203 bool eq; 204 205 eq = otp_devices_equal (&tp, 206 &etp); 207 GNUNET_free (etp.otp_description); 208 GNUNET_free (etp.otp_key); 209 TALER_MERCHANTDB_rollback (TMH_db); 210 if (! eq) 211 ret = TALER_MHD_reply_with_error ( 212 connection, 213 MHD_HTTP_CONFLICT, 214 TALER_EC_MERCHANT_PRIVATE_POST_OTP_DEVICES_CONFLICT_OTP_DEVICE_EXISTS, 215 device_id); 216 else if (NULL != etp.otp_device_pub) 217 /* repeating the request returns the key generated the first 218 time; the merchant is shown it once per request, never a 219 new one */ 220 ret = TALER_MHD_REPLY_JSON_PACK ( 221 connection, 222 MHD_HTTP_OK, 223 GNUNET_JSON_pack_string ("otp_device_pub", 224 etp.otp_device_pub)); 225 else 226 ret = TALER_MHD_reply_static (connection, 227 MHD_HTTP_NO_CONTENT, 228 NULL, 229 NULL, 230 0); 231 GNUNET_free (etp.otp_device_pub); 232 goto cleanup; 233 } 234 } /* end switch (qs) */ 235 236 switch (tp.otp_algorithm) 237 { 238 case TALER_MCA_ECDSA_CHALLENGE: 239 case TALER_MCA_EDDSA_CHALLENGE: 240 /* an insert that hit a serialization failure brings us back here 241 with the pair of the previous round still in hand */ 242 if (NULL != tp.otp_device_pub) 243 break; 244 if (GNUNET_OK != 245 TALER_otp_device_key_create (tp.otp_algorithm, 246 &tp.otp_key, 247 &tp.otp_device_pub)) 248 { 249 GNUNET_break (0); 250 TALER_MERCHANTDB_rollback (TMH_db); 251 ret = TALER_MHD_reply_with_error ( 252 connection, 253 MHD_HTTP_INTERNAL_SERVER_ERROR, 254 TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, 255 "failed to generate the OTP device key pair"); 256 goto cleanup; 257 } 258 generated_keys = true; 259 break; 260 default: 261 /* the TOTP algorithms use the key the merchant supplied */ 262 break; 263 } 264 265 qs = TALER_MERCHANTDB_insert_otp_device (TMH_db, 266 mi->settings.id, 267 device_id, 268 &tp); 269 if (GNUNET_DB_STATUS_HARD_ERROR == qs) 270 { 271 TALER_MERCHANTDB_rollback (TMH_db); 272 break; 273 } 274 if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) 275 { 276 /* A concurrent request created an OTP device with the same ID 277 between our lookup above and this INSERT. */ 278 TALER_MERCHANTDB_rollback (TMH_db); 279 ret = TALER_MHD_reply_with_error ( 280 connection, 281 MHD_HTTP_CONFLICT, 282 TALER_EC_MERCHANT_PRIVATE_POST_OTP_DEVICES_CONFLICT_OTP_DEVICE_EXISTS, 283 device_id); 284 goto cleanup; 285 } 286 if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) 287 { 288 qs = TALER_MERCHANTDB_commit (TMH_db); 289 if (GNUNET_DB_STATUS_SOFT_ERROR != qs) 290 break; 291 } 292 retry: 293 GNUNET_assert (GNUNET_DB_STATUS_SOFT_ERROR == qs); 294 TALER_MERCHANTDB_rollback (TMH_db); 295 } /* for RETRIES loop */ 296 if (qs < 0) 297 { 298 GNUNET_break (0); 299 ret = TALER_MHD_reply_with_error ( 300 connection, 301 MHD_HTTP_INTERNAL_SERVER_ERROR, 302 (GNUNET_DB_STATUS_SOFT_ERROR == qs) 303 ? TALER_EC_GENERIC_DB_SOFT_FAILURE 304 : TALER_EC_GENERIC_DB_COMMIT_FAILED, 305 NULL); 306 goto cleanup; 307 } 308 if (NULL != tp.otp_device_pub) 309 { 310 ret = TALER_MHD_REPLY_JSON_PACK ( 311 connection, 312 MHD_HTTP_OK, 313 GNUNET_JSON_pack_string ("otp_device_pub", 314 tp.otp_device_pub)); 315 } 316 else 317 ret = TALER_MHD_reply_static (connection, 318 MHD_HTTP_NO_CONTENT, 319 NULL, 320 NULL, 321 0); 322 cleanup: 323 /* TOTP keys are borrowed from the request JSON. Only a successfully 324 generated challenge key pair belongs to this handler. */ 325 if (generated_keys) 326 { 327 GNUNET_free (tp.otp_key); 328 GNUNET_free (tp.otp_device_pub); 329 } 330 GNUNET_JSON_parse_free (spec); 331 return ret; 332 } 333 334 335 /* end of taler-merchant-httpd_post-private-otp-devices.c */