diff options
author | Christian Grothoff <christian@grothoff.org> | 2021-08-02 19:38:23 +0200 |
---|---|---|
committer | Christian Grothoff <christian@grothoff.org> | 2021-08-02 19:38:23 +0200 |
commit | c4ae15c1dbecc65b99e42e6c355952e576cfafe0 (patch) | |
tree | ec465fc048591b51bac9a1254746308014ca8795 | |
parent | 2e6b8e10de123b446ee77f714c0108dce15cd83e (diff) | |
download | exchange-c4ae15c1dbecc65b99e42e6c355952e576cfafe0.tar.gz exchange-c4ae15c1dbecc65b99e42e6c355952e576cfafe0.zip |
-strengthen payto validation logic
m--------- | contrib/gana | 0 | ||||
-rw-r--r-- | src/include/taler_json_lib.h | 13 | ||||
-rw-r--r-- | src/include/taler_util.h | 13 | ||||
-rw-r--r-- | src/json/json_wire.c | 426 | ||||
-rw-r--r-- | src/util/payto.c | 398 |
5 files changed, 450 insertions, 400 deletions
diff --git a/contrib/gana b/contrib/gana | |||
Subproject 755e752e3a235df0be8d45374835b109f7843c1 | Subproject a92cff199209cbe850a2a0dc39f11a4a342c33b | ||
diff --git a/src/include/taler_json_lib.h b/src/include/taler_json_lib.h index ea621cea6..bc21957e8 100644 --- a/src/include/taler_json_lib.h +++ b/src/include/taler_json_lib.h | |||
@@ -523,19 +523,6 @@ TALER_JSON_exchange_wire_signature_check ( | |||
523 | 523 | ||
524 | 524 | ||
525 | /** | 525 | /** |
526 | * Validate payto:// account URL (only account information, | ||
527 | * wire subject and amount are ignored). | ||
528 | * | ||
529 | * @param payto_uri URL to parse | ||
530 | * @return #GNUNET_YES if @a payto_uri is a valid payto://iban URI | ||
531 | * #GNUNET_NO if @a payto_uri is a payto URI of an unsupported type (but may be valid) | ||
532 | * #GNUNET_SYSERR if the account incorrect or this is not a payto://-URI at all | ||
533 | */ | ||
534 | enum GNUNET_GenericReturnValue | ||
535 | TALER_JSON_validate_payto (const char *payto_uri); | ||
536 | |||
537 | |||
538 | /** | ||
539 | * Create a signed wire statement for the given account. | 526 | * Create a signed wire statement for the given account. |
540 | * | 527 | * |
541 | * @param payto_uri account specification | 528 | * @param payto_uri account specification |
diff --git a/src/include/taler_util.h b/src/include/taler_util.h index ad2f46a45..33c126cbc 100644 --- a/src/include/taler_util.h +++ b/src/include/taler_util.h | |||
@@ -313,6 +313,7 @@ TALER_payto_get_method (const char *payto_uri); | |||
313 | char * | 313 | char * |
314 | TALER_xtalerbank_account_from_payto (const char *payto); | 314 | TALER_xtalerbank_account_from_payto (const char *payto); |
315 | 315 | ||
316 | |||
316 | /** | 317 | /** |
317 | * Extract the subject value from the URI parameters. | 318 | * Extract the subject value from the URI parameters. |
318 | * | 319 | * |
@@ -323,6 +324,18 @@ TALER_xtalerbank_account_from_payto (const char *payto); | |||
323 | char * | 324 | char * |
324 | TALER_payto_get_subject (const char *payto_uri); | 325 | TALER_payto_get_subject (const char *payto_uri); |
325 | 326 | ||
327 | |||
328 | /** | ||
329 | * Check that a payto:// URI is well-formed. | ||
330 | * | ||
331 | * @param payto_uri the URL to check | ||
332 | * @return NULL on success, otherwise an error | ||
333 | * message to be freed by the caller! | ||
334 | */ | ||
335 | char * | ||
336 | TALER_payto_validate (const char *payto_uri); | ||
337 | |||
338 | |||
326 | /** | 339 | /** |
327 | * Possible values for a binary filter. | 340 | * Possible values for a binary filter. |
328 | */ | 341 | */ |
diff --git a/src/json/json_wire.c b/src/json/json_wire.c index a49e7a54a..3d7e8a81b 100644 --- a/src/json/json_wire.c +++ b/src/json/json_wire.c | |||
@@ -24,378 +24,6 @@ | |||
24 | #include "taler_json_lib.h" | 24 | #include "taler_json_lib.h" |
25 | 25 | ||
26 | 26 | ||
27 | /* Taken from GNU gettext */ | ||
28 | |||
29 | /** | ||
30 | * Entry in the country table. | ||
31 | */ | ||
32 | struct CountryTableEntry | ||
33 | { | ||
34 | /** | ||
35 | * 2-Character international country code. | ||
36 | */ | ||
37 | const char *code; | ||
38 | |||
39 | /** | ||
40 | * Long English name of the country. | ||
41 | */ | ||
42 | const char *english; | ||
43 | }; | ||
44 | |||
45 | |||
46 | /* Keep the following table in sync with gettext. | ||
47 | WARNING: the entries should stay sorted according to the code */ | ||
48 | /** | ||
49 | * List of country codes. | ||
50 | */ | ||
51 | static const struct CountryTableEntry country_table[] = { | ||
52 | { "AE", "U.A.E." }, | ||
53 | { "AF", "Afghanistan" }, | ||
54 | { "AL", "Albania" }, | ||
55 | { "AM", "Armenia" }, | ||
56 | { "AN", "Netherlands Antilles" }, | ||
57 | { "AR", "Argentina" }, | ||
58 | { "AT", "Austria" }, | ||
59 | { "AU", "Australia" }, | ||
60 | { "AZ", "Azerbaijan" }, | ||
61 | { "BA", "Bosnia and Herzegovina" }, | ||
62 | { "BD", "Bangladesh" }, | ||
63 | { "BE", "Belgium" }, | ||
64 | { "BG", "Bulgaria" }, | ||
65 | { "BH", "Bahrain" }, | ||
66 | { "BN", "Brunei Darussalam" }, | ||
67 | { "BO", "Bolivia" }, | ||
68 | { "BR", "Brazil" }, | ||
69 | { "BT", "Bhutan" }, | ||
70 | { "BY", "Belarus" }, | ||
71 | { "BZ", "Belize" }, | ||
72 | { "CA", "Canada" }, | ||
73 | { "CG", "Congo" }, | ||
74 | { "CH", "Switzerland" }, | ||
75 | { "CI", "Cote d'Ivoire" }, | ||
76 | { "CL", "Chile" }, | ||
77 | { "CM", "Cameroon" }, | ||
78 | { "CN", "People's Republic of China" }, | ||
79 | { "CO", "Colombia" }, | ||
80 | { "CR", "Costa Rica" }, | ||
81 | { "CS", "Serbia and Montenegro" }, | ||
82 | { "CZ", "Czech Republic" }, | ||
83 | { "DE", "Germany" }, | ||
84 | { "DK", "Denmark" }, | ||
85 | { "DO", "Dominican Republic" }, | ||
86 | { "DZ", "Algeria" }, | ||
87 | { "EC", "Ecuador" }, | ||
88 | { "EE", "Estonia" }, | ||
89 | { "EG", "Egypt" }, | ||
90 | { "ER", "Eritrea" }, | ||
91 | { "ES", "Spain" }, | ||
92 | { "ET", "Ethiopia" }, | ||
93 | { "FI", "Finland" }, | ||
94 | { "FO", "Faroe Islands" }, | ||
95 | { "FR", "France" }, | ||
96 | { "GB", "United Kingdom" }, | ||
97 | { "GD", "Caribbean" }, | ||
98 | { "GE", "Georgia" }, | ||
99 | { "GL", "Greenland" }, | ||
100 | { "GR", "Greece" }, | ||
101 | { "GT", "Guatemala" }, | ||
102 | { "HK", "Hong Kong" }, | ||
103 | { "HK", "Hong Kong S.A.R." }, | ||
104 | { "HN", "Honduras" }, | ||
105 | { "HR", "Croatia" }, | ||
106 | { "HT", "Haiti" }, | ||
107 | { "HU", "Hungary" }, | ||
108 | { "ID", "Indonesia" }, | ||
109 | { "IE", "Ireland" }, | ||
110 | { "IL", "Israel" }, | ||
111 | { "IN", "India" }, | ||
112 | { "IQ", "Iraq" }, | ||
113 | { "IR", "Iran" }, | ||
114 | { "IS", "Iceland" }, | ||
115 | { "IT", "Italy" }, | ||
116 | { "JM", "Jamaica" }, | ||
117 | { "JO", "Jordan" }, | ||
118 | { "JP", "Japan" }, | ||
119 | { "KE", "Kenya" }, | ||
120 | { "KG", "Kyrgyzstan" }, | ||
121 | { "KH", "Cambodia" }, | ||
122 | { "KR", "South Korea" }, | ||
123 | { "KW", "Kuwait" }, | ||
124 | { "KZ", "Kazakhstan" }, | ||
125 | { "LA", "Laos" }, | ||
126 | { "LB", "Lebanon" }, | ||
127 | { "LI", "Liechtenstein" }, | ||
128 | { "LK", "Sri Lanka" }, | ||
129 | { "LT", "Lithuania" }, | ||
130 | { "LU", "Luxembourg" }, | ||
131 | { "LV", "Latvia" }, | ||
132 | { "LY", "Libya" }, | ||
133 | { "MA", "Morocco" }, | ||
134 | { "MC", "Principality of Monaco" }, | ||
135 | { "MD", "Moldava" }, | ||
136 | { "MD", "Moldova" }, | ||
137 | { "ME", "Montenegro" }, | ||
138 | { "MK", "Former Yugoslav Republic of Macedonia" }, | ||
139 | { "ML", "Mali" }, | ||
140 | { "MM", "Myanmar" }, | ||
141 | { "MN", "Mongolia" }, | ||
142 | { "MO", "Macau S.A.R." }, | ||
143 | { "MT", "Malta" }, | ||
144 | { "MV", "Maldives" }, | ||
145 | { "MX", "Mexico" }, | ||
146 | { "MY", "Malaysia" }, | ||
147 | { "NG", "Nigeria" }, | ||
148 | { "NI", "Nicaragua" }, | ||
149 | { "NL", "Netherlands" }, | ||
150 | { "NO", "Norway" }, | ||
151 | { "NP", "Nepal" }, | ||
152 | { "NZ", "New Zealand" }, | ||
153 | { "OM", "Oman" }, | ||
154 | { "PA", "Panama" }, | ||
155 | { "PE", "Peru" }, | ||
156 | { "PH", "Philippines" }, | ||
157 | { "PK", "Islamic Republic of Pakistan" }, | ||
158 | { "PL", "Poland" }, | ||
159 | { "PR", "Puerto Rico" }, | ||
160 | { "PT", "Portugal" }, | ||
161 | { "PY", "Paraguay" }, | ||
162 | { "QA", "Qatar" }, | ||
163 | { "RE", "Reunion" }, | ||
164 | { "RO", "Romania" }, | ||
165 | { "RS", "Serbia" }, | ||
166 | { "RU", "Russia" }, | ||
167 | { "RW", "Rwanda" }, | ||
168 | { "SA", "Saudi Arabia" }, | ||
169 | { "SE", "Sweden" }, | ||
170 | { "SG", "Singapore" }, | ||
171 | { "SI", "Slovenia" }, | ||
172 | { "SK", "Slovak" }, | ||
173 | { "SN", "Senegal" }, | ||
174 | { "SO", "Somalia" }, | ||
175 | { "SR", "Suriname" }, | ||
176 | { "SV", "El Salvador" }, | ||
177 | { "SY", "Syria" }, | ||
178 | { "TH", "Thailand" }, | ||
179 | { "TJ", "Tajikistan" }, | ||
180 | { "TM", "Turkmenistan" }, | ||
181 | { "TN", "Tunisia" }, | ||
182 | { "TR", "Turkey" }, | ||
183 | { "TT", "Trinidad and Tobago" }, | ||
184 | { "TW", "Taiwan" }, | ||
185 | { "TZ", "Tanzania" }, | ||
186 | { "UA", "Ukraine" }, | ||
187 | { "US", "United States" }, | ||
188 | { "UY", "Uruguay" }, | ||
189 | { "VA", "Vatican" }, | ||
190 | { "VE", "Venezuela" }, | ||
191 | { "VN", "Viet Nam" }, | ||
192 | { "YE", "Yemen" }, | ||
193 | { "ZA", "South Africa" }, | ||
194 | { "ZW", "Zimbabwe" } | ||
195 | }; | ||
196 | |||
197 | |||
198 | /** | ||
199 | * Country code comparator function, for binary search with bsearch(). | ||
200 | * | ||
201 | * @param ptr1 pointer to a `struct table_entry` | ||
202 | * @param ptr2 pointer to a `struct table_entry` | ||
203 | * @return result of memcmp()'ing the 2-digit country codes of the entries | ||
204 | */ | ||
205 | static int | ||
206 | cmp_country_code (const void *ptr1, | ||
207 | const void *ptr2) | ||
208 | { | ||
209 | const struct CountryTableEntry *cc1 = ptr1; | ||
210 | const struct CountryTableEntry *cc2 = ptr2; | ||
211 | |||
212 | return memcmp (cc1->code, | ||
213 | cc2->code, | ||
214 | 2); | ||
215 | } | ||
216 | |||
217 | |||
218 | /** | ||
219 | * Validates given IBAN according to the European Banking Standards. See: | ||
220 | * http://www.europeanpaymentscouncil.eu/documents/ECBS%20IBAN%20standard%20EBS204_V3.2.pdf | ||
221 | * | ||
222 | * @param iban the IBAN number to validate | ||
223 | * @return #GNUNET_YES if correctly formatted; #GNUNET_NO if not | ||
224 | */ | ||
225 | static enum GNUNET_GenericReturnValue | ||
226 | validate_iban (const char *iban) | ||
227 | { | ||
228 | char cc[2]; | ||
229 | char ibancpy[35]; | ||
230 | struct CountryTableEntry cc_entry; | ||
231 | unsigned int len; | ||
232 | char *nbuf; | ||
233 | unsigned long long dividend; | ||
234 | unsigned long long remainder; | ||
235 | unsigned int i; | ||
236 | unsigned int j; | ||
237 | |||
238 | len = strlen (iban); | ||
239 | if (len > 34) | ||
240 | { | ||
241 | GNUNET_log (GNUNET_ERROR_TYPE_INFO, | ||
242 | "IBAN number too long to be valid\n"); | ||
243 | return GNUNET_NO; | ||
244 | } | ||
245 | memcpy (cc, iban, 2); | ||
246 | memcpy (ibancpy, iban + 4, len - 4); | ||
247 | memcpy (ibancpy + len - 4, iban, 4); | ||
248 | ibancpy[len] = '\0'; | ||
249 | cc_entry.code = cc; | ||
250 | cc_entry.english = NULL; | ||
251 | if (NULL == | ||
252 | bsearch (&cc_entry, | ||
253 | country_table, | ||
254 | sizeof (country_table) / sizeof (struct CountryTableEntry), | ||
255 | sizeof (struct CountryTableEntry), | ||
256 | &cmp_country_code)) | ||
257 | { | ||
258 | GNUNET_log (GNUNET_ERROR_TYPE_INFO, | ||
259 | "Country code `%c%c' not supported\n", | ||
260 | cc[0], | ||
261 | cc[1]); | ||
262 | return GNUNET_NO; | ||
263 | } | ||
264 | nbuf = GNUNET_malloc ((len * 2) + 1); | ||
265 | for (i = 0, j = 0; i < len; i++) | ||
266 | { | ||
267 | if (isalpha ((unsigned char) ibancpy[i])) | ||
268 | { | ||
269 | if (2 != snprintf (&nbuf[j], | ||
270 | 3, | ||
271 | "%2u", | ||
272 | (ibancpy[i] - 'A' + 10))) | ||
273 | { | ||
274 | GNUNET_free (nbuf); | ||
275 | return GNUNET_NO; | ||
276 | } | ||
277 | j += 2; | ||
278 | continue; | ||
279 | } | ||
280 | nbuf[j] = ibancpy[i]; | ||
281 | j++; | ||
282 | } | ||
283 | for (j = 0; '\0' != nbuf[j]; j++) | ||
284 | { | ||
285 | if (! isdigit ( (unsigned char) nbuf[j])) | ||
286 | { | ||
287 | GNUNET_log (GNUNET_ERROR_TYPE_WARNING, | ||
288 | "IBAN `%s' didn't convert to numeric format\n", | ||
289 | iban); | ||
290 | return GNUNET_NO; | ||
291 | } | ||
292 | } | ||
293 | GNUNET_assert (sizeof(dividend) >= 8); | ||
294 | remainder = 0; | ||
295 | for (unsigned int i = 0; i<j; i += 16) | ||
296 | { | ||
297 | int nread; | ||
298 | |||
299 | if (1 != | ||
300 | sscanf (&nbuf[i], | ||
301 | "%16llu %n", | ||
302 | ÷nd, | ||
303 | &nread)) | ||
304 | { | ||
305 | GNUNET_free (nbuf); | ||
306 | GNUNET_break_op (0); | ||
307 | return GNUNET_NO; | ||
308 | } | ||
309 | if (0 != remainder) | ||
310 | dividend += remainder * (pow (10, nread)); | ||
311 | remainder = dividend % 97; | ||
312 | } | ||
313 | GNUNET_free (nbuf); | ||
314 | if (1 != remainder) | ||
315 | { | ||
316 | GNUNET_log (GNUNET_ERROR_TYPE_WARNING, | ||
317 | "IBAN `%s' has the wrong checksum\n", | ||
318 | iban); | ||
319 | return GNUNET_NO; | ||
320 | } | ||
321 | return GNUNET_YES; | ||
322 | } | ||
323 | |||
324 | |||
325 | /** | ||
326 | * Validate payto://iban/ account URL (only account information, | ||
327 | * wire subject and amount are ignored). | ||
328 | * | ||
329 | * @param account_url URL to parse | ||
330 | * @return #GNUNET_YES if @a account_url is a valid payto://iban URI, | ||
331 | * #GNUNET_NO if @a account_url is a payto URI of a different type, | ||
332 | * #GNUNET_SYSERR if the IBAN (checksum) is incorrect or this is not a payto://-URI | ||
333 | */ | ||
334 | static enum GNUNET_GenericReturnValue | ||
335 | validate_payto_iban (const char *account_url) | ||
336 | { | ||
337 | const char *iban; | ||
338 | const char *q; | ||
339 | char *result; | ||
340 | |||
341 | #define IBAN_PREFIX "payto://iban/" | ||
342 | if (0 != strncasecmp (account_url, | ||
343 | IBAN_PREFIX, | ||
344 | strlen (IBAN_PREFIX))) | ||
345 | return GNUNET_NO; | ||
346 | |||
347 | iban = strrchr (account_url, '/') + 1; | ||
348 | #undef IBAN_PREFIX | ||
349 | q = strchr (iban, | ||
350 | '?'); | ||
351 | if (NULL != q) | ||
352 | { | ||
353 | result = GNUNET_strndup (iban, | ||
354 | q - iban); | ||
355 | } | ||
356 | else | ||
357 | { | ||
358 | result = GNUNET_strdup (iban); | ||
359 | } | ||
360 | if (GNUNET_OK != | ||
361 | validate_iban (result)) | ||
362 | { | ||
363 | GNUNET_free (result); | ||
364 | return GNUNET_SYSERR; | ||
365 | } | ||
366 | GNUNET_free (result); | ||
367 | return GNUNET_YES; | ||
368 | } | ||
369 | |||
370 | |||
371 | enum GNUNET_GenericReturnValue | ||
372 | TALER_JSON_validate_payto (const char *payto_uri) | ||
373 | { | ||
374 | enum GNUNET_GenericReturnValue ret; | ||
375 | const char *start; | ||
376 | const char *end; | ||
377 | |||
378 | #define PAYTO_PREFIX "payto://" | ||
379 | if (0 != strncasecmp (payto_uri, | ||
380 | PAYTO_PREFIX, | ||
381 | strlen (PAYTO_PREFIX))) | ||
382 | return GNUNET_SYSERR; /* not payto */ | ||
383 | start = &payto_uri[strlen (PAYTO_PREFIX)]; | ||
384 | #undef PAYTO_PREFIX | ||
385 | end = strchr (start, | ||
386 | (unsigned char) '/'); | ||
387 | if (NULL == end) | ||
388 | return GNUNET_SYSERR; | ||
389 | if (GNUNET_NO != (ret = validate_payto_iban (payto_uri))) | ||
390 | { | ||
391 | GNUNET_break_op (GNUNET_SYSERR != ret); | ||
392 | return ret; /* got a definitive answer */ | ||
393 | } | ||
394 | /* Insert other bank account validation methods here later! */ | ||
395 | return GNUNET_NO; | ||
396 | } | ||
397 | |||
398 | |||
399 | /** | 27 | /** |
400 | * Compute the hash of the given wire details. The resulting | 28 | * Compute the hash of the given wire details. The resulting |
401 | * hash is what is put into the contract. | 29 | * hash is what is put into the contract. |
@@ -429,11 +57,19 @@ TALER_JSON_merchant_wire_signature_hash (const json_t *wire_s, | |||
429 | GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, | 57 | GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, |
430 | "Validating `%s'\n", | 58 | "Validating `%s'\n", |
431 | payto_uri); | 59 | payto_uri); |
432 | if (GNUNET_SYSERR == | ||
433 | TALER_JSON_validate_payto (payto_uri)) | ||
434 | { | 60 | { |
435 | GNUNET_break_op (0); | 61 | char *err; |
436 | return GNUNET_SYSERR; | 62 | |
63 | err = TALER_payto_validate (payto_uri); | ||
64 | if (NULL != err) | ||
65 | { | ||
66 | GNUNET_log (GNUNET_ERROR_TYPE_WARNING, | ||
67 | "URI `%s' ill-formed: %s\n", | ||
68 | payto_uri, | ||
69 | err); | ||
70 | GNUNET_free (err); | ||
71 | return GNUNET_SYSERR; | ||
72 | } | ||
437 | } | 73 | } |
438 | TALER_merchant_wire_signature_hash (payto_uri, | 74 | TALER_merchant_wire_signature_hash (payto_uri, |
439 | salt, | 75 | salt, |
@@ -472,11 +108,19 @@ TALER_JSON_exchange_wire_signature_check ( | |||
472 | return GNUNET_SYSERR; | 108 | return GNUNET_SYSERR; |
473 | } | 109 | } |
474 | 110 | ||
475 | if (GNUNET_SYSERR == | ||
476 | TALER_JSON_validate_payto (payto_uri)) | ||
477 | { | 111 | { |
478 | GNUNET_break_op (0); | 112 | char *err; |
479 | return GNUNET_SYSERR; | 113 | |
114 | err = TALER_payto_validate (payto_uri); | ||
115 | if (NULL != err) | ||
116 | { | ||
117 | GNUNET_log (GNUNET_ERROR_TYPE_WARNING, | ||
118 | "URI `%s' ill-formed: %s\n", | ||
119 | payto_uri, | ||
120 | err); | ||
121 | GNUNET_free (err); | ||
122 | return GNUNET_SYSERR; | ||
123 | } | ||
480 | } | 124 | } |
481 | 125 | ||
482 | return TALER_exchange_wire_signature_check (payto_uri, | 126 | return TALER_exchange_wire_signature_check (payto_uri, |
@@ -498,11 +142,16 @@ TALER_JSON_exchange_wire_signature_make ( | |||
498 | const struct TALER_MasterPrivateKeyP *master_priv) | 142 | const struct TALER_MasterPrivateKeyP *master_priv) |
499 | { | 143 | { |
500 | struct TALER_MasterSignatureP master_sig; | 144 | struct TALER_MasterSignatureP master_sig; |
145 | char *err; | ||
501 | 146 | ||
502 | if (GNUNET_SYSERR == | 147 | if (NULL != |
503 | TALER_JSON_validate_payto (payto_uri)) | 148 | (err = TALER_payto_validate (payto_uri))) |
504 | { | 149 | { |
505 | GNUNET_break_op (0); | 150 | GNUNET_log (GNUNET_ERROR_TYPE_ERROR, |
151 | "Invalid payto URI `%s': %s\n", | ||
152 | payto_uri, | ||
153 | err); | ||
154 | GNUNET_free (err); | ||
506 | return NULL; | 155 | return NULL; |
507 | } | 156 | } |
508 | TALER_exchange_wire_signature_make (payto_uri, | 157 | TALER_exchange_wire_signature_make (payto_uri, |
@@ -528,6 +177,7 @@ TALER_JSON_wire_to_payto (const json_t *wire_s) | |||
528 | { | 177 | { |
529 | json_t *payto_o; | 178 | json_t *payto_o; |
530 | const char *payto_str; | 179 | const char *payto_str; |
180 | char *err; | ||
531 | 181 | ||
532 | payto_o = json_object_get (wire_s, | 182 | payto_o = json_object_get (wire_s, |
533 | "payto_uri"); | 183 | "payto_uri"); |
@@ -538,12 +188,14 @@ TALER_JSON_wire_to_payto (const json_t *wire_s) | |||
538 | "Malformed wire record encountered: lacks payto://-url\n"); | 188 | "Malformed wire record encountered: lacks payto://-url\n"); |
539 | return NULL; | 189 | return NULL; |
540 | } | 190 | } |
541 | if (GNUNET_SYSERR == | 191 | if (NULL != |
542 | TALER_JSON_validate_payto (payto_str)) | 192 | (err = TALER_payto_validate (payto_str))) |
543 | { | 193 | { |
544 | GNUNET_log (GNUNET_ERROR_TYPE_ERROR, | 194 | GNUNET_log (GNUNET_ERROR_TYPE_ERROR, |
545 | "Malformed wire record encountered: payto URI `%s' invalid\n", | 195 | "Malformed wire record encountered: payto URI `%s' invalid: %s\n", |
546 | payto_str); | 196 | payto_str, |
197 | err); | ||
198 | GNUNET_free (err); | ||
547 | return NULL; | 199 | return NULL; |
548 | } | 200 | } |
549 | return GNUNET_strdup (payto_str); | 201 | return GNUNET_strdup (payto_str); |
diff --git a/src/util/payto.c b/src/util/payto.c index 54072d667..7819184e8 100644 --- a/src/util/payto.c +++ b/src/util/payto.c | |||
@@ -128,3 +128,401 @@ TALER_xtalerbank_account_from_payto (const char *payto) | |||
128 | return GNUNET_strndup (beg, | 128 | return GNUNET_strndup (beg, |
129 | end - beg); | 129 | end - beg); |
130 | } | 130 | } |
131 | |||
132 | |||
133 | /* Country table taken from GNU gettext */ | ||
134 | |||
135 | /** | ||
136 | * Entry in the country table. | ||
137 | */ | ||
138 | struct CountryTableEntry | ||
139 | { | ||
140 | /** | ||
141 | * 2-Character international country code. | ||
142 | */ | ||
143 | const char *code; | ||
144 | |||
145 | /** | ||
146 | * Long English name of the country. | ||
147 | */ | ||
148 | const char *english; | ||
149 | }; | ||
150 | |||
151 | |||
152 | /* Keep the following table in sync with gettext. | ||
153 | WARNING: the entries should stay sorted according to the code */ | ||
154 | /** | ||
155 | * List of country codes. | ||
156 | */ | ||
157 | static const struct CountryTableEntry country_table[] = { | ||
158 | { "AE", "U.A.E." }, | ||
159 | { "AF", "Afghanistan" }, | ||
160 | { "AL", "Albania" }, | ||
161 | { "AM", "Armenia" }, | ||
162 | { "AN", "Netherlands Antilles" }, | ||
163 | { "AR", "Argentina" }, | ||
164 | { "AT", "Austria" }, | ||
165 | { "AU", "Australia" }, | ||
166 | { "AZ", "Azerbaijan" }, | ||
167 | { "BA", "Bosnia and Herzegovina" }, | ||
168 | { "BD", "Bangladesh" }, | ||
169 | { "BE", "Belgium" }, | ||
170 | { "BG", "Bulgaria" }, | ||
171 | { "BH", "Bahrain" }, | ||
172 | { "BN", "Brunei Darussalam" }, | ||
173 | { "BO", "Bolivia" }, | ||
174 | { "BR", "Brazil" }, | ||
175 | { "BT", "Bhutan" }, | ||
176 | { "BY", "Belarus" }, | ||
177 | { "BZ", "Belize" }, | ||
178 | { "CA", "Canada" }, | ||
179 | { "CG", "Congo" }, | ||
180 | { "CH", "Switzerland" }, | ||
181 | { "CI", "Cote d'Ivoire" }, | ||
182 | { "CL", "Chile" }, | ||
183 | { "CM", "Cameroon" }, | ||
184 | { "CN", "People's Republic of China" }, | ||
185 | { "CO", "Colombia" }, | ||
186 | { "CR", "Costa Rica" }, | ||
187 | { "CS", "Serbia and Montenegro" }, | ||
188 | { "CZ", "Czech Republic" }, | ||
189 | { "DE", "Germany" }, | ||
190 | { "DK", "Denmark" }, | ||
191 | { "DO", "Dominican Republic" }, | ||
192 | { "DZ", "Algeria" }, | ||
193 | { "EC", "Ecuador" }, | ||
194 | { "EE", "Estonia" }, | ||
195 | { "EG", "Egypt" }, | ||
196 | { "ER", "Eritrea" }, | ||
197 | { "ES", "Spain" }, | ||
198 | { "ET", "Ethiopia" }, | ||
199 | { "FI", "Finland" }, | ||
200 | { "FO", "Faroe Islands" }, | ||
201 | { "FR", "France" }, | ||
202 | { "GB", "United Kingdom" }, | ||
203 | { "GD", "Caribbean" }, | ||
204 | { "GE", "Georgia" }, | ||
205 | { "GL", "Greenland" }, | ||
206 | { "GR", "Greece" }, | ||
207 | { "GT", "Guatemala" }, | ||
208 | { "HK", "Hong Kong" }, | ||
209 | { "HK", "Hong Kong S.A.R." }, | ||
210 | { "HN", "Honduras" }, | ||
211 | { "HR", "Croatia" }, | ||
212 | { "HT", "Haiti" }, | ||
213 | { "HU", "Hungary" }, | ||
214 | { "ID", "Indonesia" }, | ||
215 | { "IE", "Ireland" }, | ||
216 | { "IL", "Israel" }, | ||
217 | { "IN", "India" }, | ||
218 | { "IQ", "Iraq" }, | ||
219 | { "IR", "Iran" }, | ||
220 | { "IS", "Iceland" }, | ||
221 | { "IT", "Italy" }, | ||
222 | { "JM", "Jamaica" }, | ||
223 | { "JO", "Jordan" }, | ||
224 | { "JP", "Japan" }, | ||
225 | { "KE", "Kenya" }, | ||
226 | { "KG", "Kyrgyzstan" }, | ||
227 | { "KH", "Cambodia" }, | ||
228 | { "KR", "South Korea" }, | ||
229 | { "KW", "Kuwait" }, | ||
230 | { "KZ", "Kazakhstan" }, | ||
231 | { "LA", "Laos" }, | ||
232 | { "LB", "Lebanon" }, | ||
233 | { "LI", "Liechtenstein" }, | ||
234 | { "LK", "Sri Lanka" }, | ||
235 | { "LT", "Lithuania" }, | ||
236 | { "LU", "Luxembourg" }, | ||
237 | { "LV", "Latvia" }, | ||
238 | { "LY", "Libya" }, | ||
239 | { "MA", "Morocco" }, | ||
240 | { "MC", "Principality of Monaco" }, | ||
241 | { "MD", "Moldava" }, | ||
242 | { "MD", "Moldova" }, | ||
243 | { "ME", "Montenegro" }, | ||
244 | { "MK", "Former Yugoslav Republic of Macedonia" }, | ||
245 | { "ML", "Mali" }, | ||
246 | { "MM", "Myanmar" }, | ||
247 | { "MN", "Mongolia" }, | ||
248 | { "MO", "Macau S.A.R." }, | ||
249 | { "MT", "Malta" }, | ||
250 | { "MV", "Maldives" }, | ||
251 | { "MX", "Mexico" }, | ||
252 | { "MY", "Malaysia" }, | ||
253 | { "NG", "Nigeria" }, | ||
254 | { "NI", "Nicaragua" }, | ||
255 | { "NL", "Netherlands" }, | ||
256 | { "NO", "Norway" }, | ||
257 | { "NP", "Nepal" }, | ||
258 | { "NZ", "New Zealand" }, | ||
259 | { "OM", "Oman" }, | ||
260 | { "PA", "Panama" }, | ||
261 | { "PE", "Peru" }, | ||
262 | { "PH", "Philippines" }, | ||
263 | { "PK", "Islamic Republic of Pakistan" }, | ||
264 | { "PL", "Poland" }, | ||
265 | { "PR", "Puerto Rico" }, | ||
266 | { "PT", "Portugal" }, | ||
267 | { "PY", "Paraguay" }, | ||
268 | { "QA", "Qatar" }, | ||
269 | { "RE", "Reunion" }, | ||
270 | { "RO", "Romania" }, | ||
271 | { "RS", "Serbia" }, | ||
272 | { "RU", "Russia" }, | ||
273 | { "RW", "Rwanda" }, | ||
274 | { "SA", "Saudi Arabia" }, | ||
275 | { "SE", "Sweden" }, | ||
276 | { "SG", "Singapore" }, | ||
277 | { "SI", "Slovenia" }, | ||
278 | { "SK", "Slovak" }, | ||
279 | { "SN", "Senegal" }, | ||
280 | { "SO", "Somalia" }, | ||
281 | { "SR", "Suriname" }, | ||
282 | { "SV", "El Salvador" }, | ||
283 | { "SY", "Syria" }, | ||
284 | { "TH", "Thailand" }, | ||
285 | { "TJ", "Tajikistan" }, | ||
286 | { "TM", "Turkmenistan" }, | ||
287 | { "TN", "Tunisia" }, | ||
288 | { "TR", "Turkey" }, | ||
289 | { "TT", "Trinidad and Tobago" }, | ||
290 | { "TW", "Taiwan" }, | ||
291 | { "TZ", "Tanzania" }, | ||
292 | { "UA", "Ukraine" }, | ||
293 | { "US", "United States" }, | ||
294 | { "UY", "Uruguay" }, | ||
295 | { "VA", "Vatican" }, | ||
296 | { "VE", "Venezuela" }, | ||
297 | { "VN", "Viet Nam" }, | ||
298 | { "YE", "Yemen" }, | ||
299 | { "ZA", "South Africa" }, | ||
300 | { "ZW", "Zimbabwe" } | ||
301 | }; | ||
302 | |||
303 | |||
304 | /** | ||
305 | * Country code comparator function, for binary search with bsearch(). | ||
306 | * | ||
307 | * @param ptr1 pointer to a `struct table_entry` | ||
308 | * @param ptr2 pointer to a `struct table_entry` | ||
309 | * @return result of memcmp()'ing the 2-digit country codes of the entries | ||
310 | */ | ||
311 | static int | ||
312 | cmp_country_code (const void *ptr1, | ||
313 | const void *ptr2) | ||
314 | { | ||
315 | const struct CountryTableEntry *cc1 = ptr1; | ||
316 | const struct CountryTableEntry *cc2 = ptr2; | ||
317 | |||
318 | return memcmp (cc1->code, | ||
319 | cc2->code, | ||
320 | 2); | ||
321 | } | ||
322 | |||
323 | |||
324 | /** | ||
325 | * Validates given IBAN according to the European Banking Standards. See: | ||
326 | * http://www.europeanpaymentscouncil.eu/documents/ECBS%20IBAN%20standard%20EBS204_V3.2.pdf | ||
327 | * | ||
328 | * @param iban the IBAN number to validate | ||
329 | * @return NULL if correctly formatted; error message if not | ||
330 | */ | ||
331 | static char * | ||
332 | validate_iban (const char *iban) | ||
333 | { | ||
334 | char cc[2]; | ||
335 | char ibancpy[35]; | ||
336 | struct CountryTableEntry cc_entry; | ||
337 | unsigned int len; | ||
338 | char *nbuf; | ||
339 | unsigned long long dividend; | ||
340 | unsigned long long remainder; | ||
341 | unsigned int i; | ||
342 | unsigned int j; | ||
343 | |||
344 | len = strlen (iban); | ||
345 | if (len > 34) | ||
346 | return GNUNET_strdup ("IBAN number too long to be valid"); | ||
347 | memcpy (cc, iban, 2); | ||
348 | memcpy (ibancpy, iban + 4, len - 4); | ||
349 | memcpy (ibancpy + len - 4, iban, 4); | ||
350 | ibancpy[len] = '\0'; | ||
351 | cc_entry.code = cc; | ||
352 | cc_entry.english = NULL; | ||
353 | if (NULL == | ||
354 | bsearch (&cc_entry, | ||
355 | country_table, | ||
356 | sizeof (country_table) / sizeof (struct CountryTableEntry), | ||
357 | sizeof (struct CountryTableEntry), | ||
358 | &cmp_country_code)) | ||
359 | { | ||
360 | char *msg; | ||
361 | |||
362 | GNUNET_asprintf (&msg, | ||
363 | "Country code `%c%c' not supported\n", | ||
364 | cc[0], | ||
365 | cc[1]); | ||
366 | return msg; | ||
367 | } | ||
368 | nbuf = GNUNET_malloc ((len * 2) + 1); | ||
369 | for (i = 0, j = 0; i < len; i++) | ||
370 | { | ||
371 | if (isalpha ((unsigned char) ibancpy[i])) | ||
372 | { | ||
373 | if (2 != snprintf (&nbuf[j], | ||
374 | 3, | ||
375 | "%2u", | ||
376 | (ibancpy[i] - 'A' + 10))) | ||
377 | { | ||
378 | GNUNET_break (0); | ||
379 | return GNUNET_strdup ("internal invariant violation"); | ||
380 | } | ||
381 | j += 2; | ||
382 | continue; | ||
383 | } | ||
384 | nbuf[j] = ibancpy[i]; | ||
385 | j++; | ||
386 | } | ||
387 | for (j = 0; '\0' != nbuf[j]; j++) | ||
388 | { | ||
389 | if (! isdigit ( (unsigned char) nbuf[j])) | ||
390 | { | ||
391 | char *msg; | ||
392 | |||
393 | GNUNET_asprintf (&msg, | ||
394 | "digit expected at `%s'", | ||
395 | &nbuf[j]); | ||
396 | GNUNET_free (nbuf); | ||
397 | return msg; | ||
398 | } | ||
399 | } | ||
400 | GNUNET_assert (sizeof(dividend) >= 8); | ||
401 | remainder = 0; | ||
402 | for (unsigned int i = 0; i<j; i += 16) | ||
403 | { | ||
404 | int nread; | ||
405 | |||
406 | if (1 != | ||
407 | sscanf (&nbuf[i], | ||
408 | "%16llu %n", | ||
409 | ÷nd, | ||
410 | &nread)) | ||
411 | { | ||
412 | char *msg; | ||
413 | |||
414 | GNUNET_asprintf (&msg, | ||
415 | "wrong input for checksum calculation at `%s'", | ||
416 | &nbuf[i]); | ||
417 | GNUNET_free (nbuf); | ||
418 | return msg; | ||
419 | } | ||
420 | if (0 != remainder) | ||
421 | dividend += remainder * (pow (10, nread)); | ||
422 | remainder = dividend % 97; | ||
423 | } | ||
424 | GNUNET_free (nbuf); | ||
425 | if (1 != remainder) | ||
426 | return GNUNET_strdup ("IBAN checksum is wrong"); | ||
427 | return NULL; | ||
428 | } | ||
429 | |||
430 | |||
431 | /** | ||
432 | * Validate payto://iban/ account URL (only account information, | ||
433 | * wire subject and amount are ignored). | ||
434 | * | ||
435 | * @param account_url URL to parse | ||
436 | * @return NULL on success, otherwise an error message | ||
437 | * to be freed by the caller | ||
438 | */ | ||
439 | static char * | ||
440 | validate_payto_iban (const char *account_url) | ||
441 | { | ||
442 | const char *iban; | ||
443 | const char *q; | ||
444 | char *result; | ||
445 | char *err; | ||
446 | |||
447 | #define IBAN_PREFIX "payto://iban/" | ||
448 | if (0 != strncasecmp (account_url, | ||
449 | IBAN_PREFIX, | ||
450 | strlen (IBAN_PREFIX))) | ||
451 | return NULL; /* not an IBAN */ | ||
452 | |||
453 | iban = strrchr (account_url, '/') + 1; | ||
454 | #undef IBAN_PREFIX | ||
455 | q = strchr (iban, | ||
456 | '?'); | ||
457 | if (NULL != q) | ||
458 | { | ||
459 | result = GNUNET_strndup (iban, | ||
460 | q - iban); | ||
461 | } | ||
462 | else | ||
463 | { | ||
464 | result = GNUNET_strdup (iban); | ||
465 | } | ||
466 | if (NULL != | ||
467 | (err = validate_iban (result))) | ||
468 | { | ||
469 | GNUNET_free (result); | ||
470 | return err; | ||
471 | } | ||
472 | GNUNET_free (result); | ||
473 | return NULL; | ||
474 | } | ||
475 | |||
476 | |||
477 | /** | ||
478 | * Check that a payto:// URI is well-formed. | ||
479 | * | ||
480 | * @param payto_uri the URL to check | ||
481 | * @return NULL on success, otherwise an error | ||
482 | * message to be freed by the caller! | ||
483 | */ | ||
484 | char * | ||
485 | TALER_payto_validate (const char *payto_uri) | ||
486 | { | ||
487 | char *ret; | ||
488 | const char *start; | ||
489 | const char *end; | ||
490 | |||
491 | if (0 != strncasecmp (payto_uri, | ||
492 | PAYTO, | ||
493 | strlen (PAYTO))) | ||
494 | return GNUNET_strdup ("invalid prefix"); | ||
495 | for (unsigned int i = 0; '\0' != payto_uri[i]; i++) | ||
496 | { | ||
497 | /* This is more strict than RFC 8905, alas we do not need to support messages/instructions/etc., | ||
498 | and it is generally better to start with a narrow whitelist; we can be more permissive later ...*/ | ||
499 | #define ALLOWED_CHARACTERS \ | ||
500 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/:&?-.,=" | ||
501 | if (NULL == strchr (ALLOWED_CHARACTERS, | ||
502 | (int) payto_uri[i])) | ||
503 | { | ||
504 | char *ret; | ||
505 | |||
506 | GNUNET_asprintf (&ret, | ||
507 | "Encountered invalid character `%c' at offset %u in payto URI `%s'", | ||
508 | payto_uri[i], | ||
509 | i, | ||
510 | payto_uri); | ||
511 | return ret; | ||
512 | } | ||
513 | #undef ALLOWED_CHARACTERS | ||
514 | } | ||
515 | |||
516 | start = &payto_uri[strlen (PAYTO)]; | ||
517 | end = strchr (start, | ||
518 | (unsigned char) '/'); | ||
519 | if (NULL == end) | ||
520 | return GNUNET_strdup ("missing '/' in payload"); | ||
521 | |||
522 | if (NULL != (ret = validate_payto_iban (payto_uri))) | ||
523 | return ret; /* got a definitive answer */ | ||
524 | |||
525 | /* Insert other bank account validation methods here later! */ | ||
526 | |||
527 | return NULL; | ||
528 | } | ||