taler-merchant-httpd_post-private-otp-devices.c (10645B)
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 121 switch (tp.otp_algorithm) 122 { 123 case TALER_MCA_ECDSA_CHALLENGE: 124 case TALER_MCA_EDDSA_CHALLENGE: 125 if (NULL != tp.otp_key) 126 { 127 GNUNET_break_op (0); 128 ret = TALER_MHD_reply_with_error (connection, 129 MHD_HTTP_BAD_REQUEST, 130 TALER_EC_GENERIC_PARAMETER_MALFORMED, 131 "otp_key"); 132 goto cleanup; 133 } 134 break; 135 case TALER_MCA_NONE: 136 case TALER_MCA_WITHOUT_PRICE: 137 case TALER_MCA_WITH_PRICE: 138 /* the merchant shares these with the OTP application itself */ 139 if (NULL == tp.otp_key) 140 { 141 GNUNET_break_op (0); 142 ret = TALER_MHD_reply_with_error (connection, 143 MHD_HTTP_BAD_REQUEST, 144 TALER_EC_GENERIC_PARAMETER_MISSING, 145 "otp_key"); 146 goto cleanup; 147 } 148 break; 149 } 150 151 /* finally, interact with DB until no serialization error */ 152 for (unsigned int i = 0; i<MAX_RETRIES; i++) 153 { 154 /* Test if a OTP device of this id is known */ 155 struct TALER_MERCHANTDB_OtpDeviceDetails etp; 156 157 if (GNUNET_OK != 158 TALER_MERCHANTDB_start (TMH_db, 159 "/post otp-devices")) 160 { 161 GNUNET_break (0); 162 ret = TALER_MHD_reply_with_error (connection, 163 MHD_HTTP_INTERNAL_SERVER_ERROR, 164 TALER_EC_GENERIC_DB_START_FAILED, 165 NULL); 166 goto cleanup; 167 } 168 qs = TALER_MERCHANTDB_get_otp_device (TMH_db, 169 mi->settings.id, 170 device_id, 171 &etp); 172 switch (qs) 173 { 174 case GNUNET_DB_STATUS_HARD_ERROR: 175 /* Clean up and fail hard */ 176 GNUNET_break (0); 177 TALER_MERCHANTDB_rollback (TMH_db); 178 ret = TALER_MHD_reply_with_error (connection, 179 MHD_HTTP_INTERNAL_SERVER_ERROR, 180 TALER_EC_GENERIC_DB_FETCH_FAILED, 181 NULL); 182 goto cleanup; 183 case GNUNET_DB_STATUS_SOFT_ERROR: 184 /* restart transaction */ 185 goto retry; 186 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 187 /* Good, we can proceed! */ 188 break; 189 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 190 /* idempotency check: is etp == tp? */ 191 { 192 bool eq; 193 194 eq = otp_devices_equal (&tp, 195 &etp); 196 GNUNET_free (etp.otp_description); 197 GNUNET_free (etp.otp_key); 198 TALER_MERCHANTDB_rollback (TMH_db); 199 if (! eq) 200 ret = TALER_MHD_reply_with_error ( 201 connection, 202 MHD_HTTP_CONFLICT, 203 TALER_EC_MERCHANT_PRIVATE_POST_OTP_DEVICES_CONFLICT_OTP_DEVICE_EXISTS, 204 device_id); 205 else if (NULL != etp.otp_device_pub) 206 /* repeating the request returns the key generated the first 207 time; the merchant is shown it once per request, never a 208 new one */ 209 ret = TALER_MHD_REPLY_JSON_PACK ( 210 connection, 211 MHD_HTTP_OK, 212 GNUNET_JSON_pack_string ("otp_device_pub", 213 etp.otp_device_pub)); 214 else 215 ret = TALER_MHD_reply_static (connection, 216 MHD_HTTP_NO_CONTENT, 217 NULL, 218 NULL, 219 0); 220 GNUNET_free (etp.otp_device_pub); 221 goto cleanup; 222 } 223 } /* end switch (qs) */ 224 225 switch (tp.otp_algorithm) 226 { 227 case TALER_MCA_ECDSA_CHALLENGE: 228 case TALER_MCA_EDDSA_CHALLENGE: 229 /* an insert that hit a serialization failure brings us back here 230 with the pair of the previous round still in hand */ 231 if (NULL != tp.otp_device_pub) 232 break; 233 if (GNUNET_OK != 234 TALER_otp_device_key_create (tp.otp_algorithm, 235 &tp.otp_key, 236 &tp.otp_device_pub)) 237 { 238 GNUNET_break (0); 239 TALER_MERCHANTDB_rollback (TMH_db); 240 ret = TALER_MHD_reply_with_error ( 241 connection, 242 MHD_HTTP_INTERNAL_SERVER_ERROR, 243 TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, 244 "failed to generate the OTP device key pair"); 245 goto cleanup; 246 } 247 generated_keys = true; 248 break; 249 default: 250 /* the TOTP algorithms use the key the merchant supplied */ 251 break; 252 } 253 254 qs = TALER_MERCHANTDB_insert_otp_device (TMH_db, 255 mi->settings.id, 256 device_id, 257 &tp); 258 if (GNUNET_DB_STATUS_HARD_ERROR == qs) 259 { 260 TALER_MERCHANTDB_rollback (TMH_db); 261 break; 262 } 263 if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) 264 { 265 /* A concurrent request created an OTP device with the same ID 266 between our lookup above and this INSERT. */ 267 TALER_MERCHANTDB_rollback (TMH_db); 268 ret = TALER_MHD_reply_with_error ( 269 connection, 270 MHD_HTTP_CONFLICT, 271 TALER_EC_MERCHANT_PRIVATE_POST_OTP_DEVICES_CONFLICT_OTP_DEVICE_EXISTS, 272 device_id); 273 goto cleanup; 274 } 275 if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) 276 { 277 qs = TALER_MERCHANTDB_commit (TMH_db); 278 if (GNUNET_DB_STATUS_SOFT_ERROR != qs) 279 break; 280 } 281 retry: 282 GNUNET_assert (GNUNET_DB_STATUS_SOFT_ERROR == qs); 283 TALER_MERCHANTDB_rollback (TMH_db); 284 } /* for RETRIES loop */ 285 if (qs < 0) 286 { 287 GNUNET_break (0); 288 ret = TALER_MHD_reply_with_error ( 289 connection, 290 MHD_HTTP_INTERNAL_SERVER_ERROR, 291 (GNUNET_DB_STATUS_SOFT_ERROR == qs) 292 ? TALER_EC_GENERIC_DB_SOFT_FAILURE 293 : TALER_EC_GENERIC_DB_COMMIT_FAILED, 294 NULL); 295 goto cleanup; 296 } 297 if (NULL != tp.otp_device_pub) 298 { 299 ret = TALER_MHD_REPLY_JSON_PACK ( 300 connection, 301 MHD_HTTP_OK, 302 GNUNET_JSON_pack_string ("otp_device_pub", 303 tp.otp_device_pub)); 304 } 305 else 306 ret = TALER_MHD_reply_static (connection, 307 MHD_HTTP_NO_CONTENT, 308 NULL, 309 NULL, 310 0); 311 cleanup: 312 /* TOTP keys are borrowed from the request JSON. Only a successfully 313 generated challenge key pair belongs to this handler. */ 314 if (generated_keys) 315 { 316 GNUNET_free (tp.otp_key); 317 GNUNET_free (tp.otp_device_pub); 318 } 319 GNUNET_JSON_parse_free (spec); 320 return ret; 321 } 322 323 324 /* end of taler-merchant-httpd_post-private-otp-devices.c */