diff options
author | Christian Blättler <blatc2@bfh.ch> | 2024-04-27 16:47:34 +0200 |
---|---|---|
committer | Christian Blättler <blatc2@bfh.ch> | 2024-04-27 16:47:34 +0200 |
commit | 5d9db0012144cbad9ba90b654fdfff17b55b6af9 (patch) | |
tree | 2ff02fb72aab11ec3c1b332ddfb59e3e4d8a2e64 | |
parent | 27ab65168f8c5024a1ad4220e7483a2b8ceeb5bb (diff) |
validate and sign token envelopes
-rw-r--r-- | src/backend/taler-merchant-httpd_contract.h | 10 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_post-orders-ID-pay.c | 426 |
2 files changed, 362 insertions, 74 deletions
diff --git a/src/backend/taler-merchant-httpd_contract.h b/src/backend/taler-merchant-httpd_contract.h index 0e52b6e2..6c1841e9 100644 --- a/src/backend/taler-merchant-httpd_contract.h +++ b/src/backend/taler-merchant-httpd_contract.h @@ -326,16 +326,6 @@ struct TALER_MerchantContractTokenFamily */ struct { - // /** - // * When does the subscription period start? - // */ - // struct GNUNET_TIME_Timestamp start_date; - - // /** - // * When does the subscription period end? - // */ - // struct GNUNET_TIME_Timestamp end_date; - /** * Array of domain names where this subscription can be safely used * (e.g. the issuer warrants that these sites will re-issue tokens of diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c index 25661a4e..1808989a 100644 --- a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c +++ b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c @@ -26,6 +26,7 @@ */ #include "platform.h" #include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_db_lib.h> #include <gnunet/gnunet_json_lib.h> #include <gnunet/gnunet_time_lib.h> #include <jansson.h> @@ -42,6 +43,7 @@ #include "taler-merchant-httpd_helper.h" #include "taler-merchant-httpd_post-orders-ID-pay.h" #include "taler-merchant-httpd_private-get-orders.h" +#include "taler_merchantdb_plugin.h" /** @@ -56,9 +58,15 @@ /** * TODO: What is a good value for this? - * Maximum number of tokens that we allow per transaction + * Maximum number of tokens that we allow as inputs per transaction */ -#define MAX_TOKEN_ALLOWED_TOKENS 128 +#define MAX_TOKEN_ALLOWED_INPUT_TOKENS 128 + +/** + * TODO: What is a good value for this? + * Maximum number of tokens that we allow as outputs per transaction + */ +#define MAX_TOKEN_ALLOWED_OUTPUT_TOKENS 128 /** * How often do we ask the exchange again about our @@ -260,6 +268,25 @@ struct TokenUseConfirmation /** + * Information about a token envelope. + */ +struct TokenEnvelope +{ + + /** + * Blinded token use public keys waiting to be signed. + */ + struct TALER_TokenEnvelopeP blinded_token; + + /** + * Hash of token issue public key. + */ + struct TALER_TokenIssuePublicKeyHashP h_issue; + +}; + + +/** * Information kept during a pay request for each exchange. */ struct ExchangeGroup @@ -330,11 +357,22 @@ struct PayContext struct DepositConfirmation *dc; /** - * Array with @e tokens_cnt tokens we are using. + * Array with @e tokens_cnt input tokens passed to this request. */ struct TokenUseConfirmation *tokens; /** + * Array with @e output_tokens_cnt signed tokens returned in + * the response to the wallet. + */ + struct TALER_TokenIssueBlindSignatureP *output_tokens; + + /** + * Array with @e token_envelopes_cnt (blinded) token envelopes. + */ + struct TokenEnvelope *token_envelopes; + + /** * Array with @e choices_len choices from the contract terms. */ struct TALER_MerchantContractChoice *choices; @@ -513,12 +551,24 @@ struct PayContext size_t coins_cnt; /** - * Number of tokens this payment uses. Length + * Number of input tokens passed to this request. Length * of the @e tokens array. */ size_t tokens_cnt; /** + * Number of token envelopes passed to this request. + * Length of the @e token_envelopes array. + */ + size_t token_envelopes_cnt; + + /** + * Number of output tokens to return in the response. + * Length of the @e output_tokens array. + */ + unsigned int output_tokens_len; + + /** * Length of the @e choices array. */ unsigned int choices_len; @@ -2077,6 +2127,8 @@ phase_execute_pay_transaction (struct PayContext *pc) } } + /* TODO: Store signed output tokens in database. */ + TMH_notify_order_change (hc->instance, TMH_OSF_CLAIMED | TMH_OSF_PAID, pc->timestamp, @@ -2135,6 +2187,186 @@ phase_execute_pay_transaction (struct PayContext *pc) /** + * Ensures that the expected number of tokens for a @e key + * are provided as inputs and have valid signatures. + */ +static enum GNUNET_GenericReturnValue +find_valid_input_tokens (struct PayContext *pc, + struct TALER_MerchantContractTokenFamilyKey *key, + unsigned int expected_num) +{ + int num_validated = 0; + struct TokenUseConfirmation *tuc = NULL; + + for (size_t i = 0; i < pc->tokens_cnt; i++) + { + if (0 != GNUNET_CRYPTO_hash_cmp (&pc->tokens[i].h_issue.hash, + &key->pub.public_key->pub_key_hash)) + { + continue; + } + + tuc = &pc->tokens[i]; + if (NULL == tuc) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Input token for public key with " + "valid_after `%s' not found\n", + GNUNET_TIME_timestamp2s (key->valid_after)); + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "'tokens' array is missing " + "required input token")); + return GNUNET_NO; + } + + if (GNUNET_OK != TALER_merchant_token_issue_verify (&tuc->pub, + &key->pub, + &tuc->unblinded_sig)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Input token for public key with valid_after " + "`%s' is invalid\n", + GNUNET_TIME_timestamp2s (key->valid_after)); + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_ISSUE_SIG_INVALID, + NULL)); + return GNUNET_NO; + } + + if (GNUNET_OK != TALER_wallet_token_use_verify (&pc->h_contract_terms, + &pc->h_wallet_data, + &tuc->pub, + &tuc->sig)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Input token for public key with valid_after " + "`%s' has invalid use signature\n", + GNUNET_TIME_timestamp2s (key->valid_after)); + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_USE_SIG_INVALID, + NULL)); + return GNUNET_NO; + } + + num_validated++; + } + + if (num_validated != expected_num) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Expected %d tokens for public key with valid_after " + "`%s', but found %d\n", + expected_num, + GNUNET_TIME_timestamp2s (key->valid_after), + num_validated); + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_COUNT_MISMATCH, + NULL)); + return GNUNET_NO; + } + + return GNUNET_YES; +} + + +static enum GNUNET_GenericReturnValue +sign_token_envelopes (struct PayContext *pc, + const char *token_family_slug, + struct TALER_MerchantContractTokenFamilyKey *key, + struct TALER_TokenIssuePrivateKeyP *priv, + unsigned int expected_num) +{ + int num_signed = 0; + + for (unsigned int i = 0; i<pc->token_envelopes_cnt; i++) + { + if (0 != GNUNET_CRYPTO_hash_cmp (&pc->token_envelopes[i].h_issue.hash, + &key->pub.public_key->pub_key_hash)) + { + continue; + } + + TALER_merchant_token_issue_sign (&pc->token_envelopes[i].blinded_token, + priv, + &pc->output_tokens[i]); + + num_signed++; + } + + if (num_signed != expected_num) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Expected %d token envelopes for public key with valid_after " + "`%s', but found %d\n", + expected_num, + GNUNET_TIME_timestamp2s (key->valid_after), + num_signed); + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_ENVELOPE_COUNT_MISMATCH, + NULL)); + return GNUNET_NO; + } + + return GNUNET_OK; +} + + +/** + * Find metching token family based on @a slug. Then use @a valid_after to + * find the matching public key. + * + * @param pc payment context + * @param slug token family slug + * @param valid_after valid_after timestamp + * @param[out] family matching token family + * @param[out] key matching public key + */ +static void +lookup_token_family_key (struct PayContext *pc, + const char *slug, + struct GNUNET_TIME_Timestamp valid_after, + struct TALER_MerchantContractTokenFamily *family, + struct TALER_MerchantContractTokenFamilyKey *key) +{ + for (unsigned int i = 0; i < pc->token_families_len; i++) + { + if (0 != strcmp (pc->token_families[i].slug, slug)) + { + continue; + } + *family = pc->token_families[i]; + for (unsigned int k = 0; k < family->keys_len; k++) + { + if (GNUNET_TIME_timestamp_cmp(family->keys[k].valid_after, + ==, + valid_after)) + { + *key = family->keys[k]; + break; + } + } + break; + } +} + + +/** * Validate tokens and token envelopes. First, we check if all tokens listed in * the 'inputs' array of the selected choice are present in the 'tokens' array * of the request. Then, we validate the signatures of each provided token. @@ -2193,42 +2425,28 @@ phase_validate_tokens (struct PayContext *pc) 1.2. Iterate over provided tokens and check if required number with matching h_issue are present. 1.3. Validate ub_sig with the issue public key, validate token_sig using the token_pub key of the request. 1.4. Sum up validated tokens and check if validated_len == tokens_cnt after loop. */ - for (unsigned int i = 0; i < selected.inputs_len; i++) + for (unsigned int i = 0; i<selected.inputs_len; i++) { - unsigned int num_validated = 0; struct TALER_MerchantContractInput input = selected.inputs[i]; + struct TALER_MerchantContractTokenFamily *family = NULL; + struct TALER_MerchantContractTokenFamilyKey *key = NULL; if (input.type != TALER_MCIT_TOKEN) { - /* only validate tokens, for now */ + /* only validate inputs of type token (for now) */ continue; } - struct TALER_MerchantContractTokenFamily *family = NULL; - struct TALER_MerchantContractTokenFamilyKey *key = NULL; - for (unsigned int j = 0; j < pc->token_families_len; j++) - { - if (0 != strcmp (pc->token_families[j].slug, input.details.token.token_family_slug)) - { - continue; - } - family = &pc->token_families[j]; - for (unsigned int k = 0; k < family->keys_len; k++) - { - if (GNUNET_TIME_timestamp_cmp(family->keys[k].valid_after, - ==, - input.details.token.valid_after)) - { - key = &family->keys[k]; - break; - } - } - break; - } + lookup_token_family_key (pc, + input.details.token.token_family_slug, + input.details.token.valid_after, + family, + key); if (NULL == family || NULL == key) { - /* this should never happen, since the choices & token families are validated on insert. */ + /* this should never happen, since the choices and + token families are validated on insert. */ GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, @@ -2238,53 +2456,80 @@ phase_validate_tokens (struct PayContext *pc) return; } - for (size_t j = 0; j < pc->tokens_cnt; j++) + if (GNUNET_NO == find_valid_input_tokens (pc, + key, + input.details.token.count)) { - if (0 != GNUNET_CRYPTO_hash_cmp (&pc->tokens[j].h_issue.hash, &key->pub.public_key->pub_key_hash)) - { - continue; - } + /* Error is already scheduled from find_valid_input_token. */ + return; + } + } - if (GNUNET_OK != TALER_merchant_token_issue_verify (&pc->tokens[j].pub, - &key->pub, - &pc->tokens[j].unblinded_sig)) - { - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_ISSUE_SIG_INVALID, - NULL)); - return; - } + GNUNET_array_grow (pc->output_tokens, + pc->output_tokens_len, + selected.outputs_len); - if (GNUNET_OK != TALER_wallet_token_use_verify (&pc->h_contract_terms, - &pc->h_wallet_data, - &pc->tokens[j].pub, - &pc->tokens[j].sig)) - { - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_USE_SIG_INVALID, - NULL)); - return; - } + for (unsigned int i = 0; i<selected.outputs_len; i++) + { + enum GNUNET_DB_QueryStatus qs; + struct TALER_MERCHANTDB_TokenFamilyKeyDetails details; + struct TALER_MerchantContractOutput output = selected.outputs[i]; + struct TALER_MerchantContractTokenFamily *family = NULL; + struct TALER_MerchantContractTokenFamilyKey *key = NULL; - num_validated++; + if (output.type != TALER_MCOT_TOKEN) + { + /* only validate outputs of type tokens (for now) */ + continue; } - if (num_validated != input.details.token.count) + lookup_token_family_key (pc, + output.details.token.token_family_slug, + output.details.token.valid_after, + family, + key); + + if (NULL == family || NULL == key) { + /* this should never happen, since the choices and + token families are validated on insert. */ GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_COUNT_MISMATCH, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, NULL)); return; } + + qs = TMH_db->lookup_token_family_key (TMH_db->cls, + pc->hc->instance->settings.id, + family->slug, + key->valid_after, + key->valid_after, + &details); + + if (qs <= 0) + { + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL)); + return; + } + + if (GNUNET_OK != sign_token_envelopes (pc, + family->slug, + key, + &details.priv, + output.details.token.count)) + { + /* Error is already scheduled from sign_token_envelopes. */ + return; + } + } } @@ -2736,6 +2981,7 @@ phase_parse_pay (struct PayContext *pc) const char *session_id = NULL; const json_t *coins; const json_t *tokens; + const json_t *tokens_evs; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_array_const ("coins", &coins), @@ -2751,6 +2997,10 @@ phase_parse_pay (struct PayContext *pc) GNUNET_JSON_spec_array_const ("tokens", &tokens), NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("tokens_evs", + &tokens_evs), + NULL), GNUNET_JSON_spec_end () }; @@ -2796,7 +3046,7 @@ phase_parse_pay (struct PayContext *pc) } pc->tokens_cnt = json_array_size (tokens); - if (pc->tokens_cnt > MAX_TOKEN_ALLOWED_TOKENS) + if (pc->tokens_cnt > MAX_TOKEN_ALLOWED_INPUT_TOKENS) { GNUNET_break_op (0); pay_end (pc, @@ -2815,6 +3065,9 @@ phase_parse_pay (struct PayContext *pc) pc->tokens = GNUNET_new_array (pc->tokens_cnt, struct TokenUseConfirmation); + pc->token_envelopes = GNUNET_new_array (pc->token_envelopes_cnt, + struct TokenEnvelope); + /* This loop populates the array 'dc' in 'pc' */ { unsigned int coins_index; @@ -2981,6 +3234,51 @@ phase_parse_pay (struct PayContext *pc) } } } + + { + unsigned int tokens_ev_index; + json_t *token_ev; + + json_array_foreach (tokens_evs, tokens_ev_index, token_ev) + { + struct TokenEnvelope *ev = &pc->token_envelopes[tokens_ev_index]; + struct GNUNET_JSON_Specification ispec[] = { + TALER_JSON_spec_token_envelope ("token_ev", + &ev->blinded_token), + GNUNET_JSON_spec_fixed_auto ("h_issue", + &ev->h_issue.hash)}; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (pc->connection, + token_ev, + ispec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); + pay_end (pc, + (GNUNET_NO == res) + ? MHD_YES + : MHD_NO); + return; + } + + for (unsigned int j = 0; j<tokens_ev_index; j++) + { + if (0 == + GNUNET_memcmp (ev->blinded_token.blinded_pub, + pc->token_envelopes[j].blinded_token.blinded_pub)) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "duplicate token envelope in list")); + return; + } + } + } + } pc->phase = PP_PARSE_WALLET_DATA; } |