diff options
author | Christian Blättler <blatc2@bfh.ch> | 2024-04-30 22:28:08 +0200 |
---|---|---|
committer | Christian Blättler <blatc2@bfh.ch> | 2024-04-30 22:28:08 +0200 |
commit | 4a3145fca389fca1bbfc7ba61b7cafd1156ef656 (patch) | |
tree | f95de17dcb3e20036dc3b6c0046cf5a2b18cc935 | |
parent | 5d9db0012144cbad9ba90b654fdfff17b55b6af9 (diff) |
work on tokens
-rw-r--r-- | src/backend/taler-merchant-httpd_contract.c | 44 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_contract.h | 22 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_post-orders-ID-pay.c | 235 | ||||
-rw-r--r-- | src/backenddb/pg_lookup_token_family_key.c | 5 | ||||
-rw-r--r-- | src/include/taler_merchant_service.h | 70 | ||||
-rw-r--r-- | src/lib/merchant_api_post_order_pay.c | 104 | ||||
-rw-r--r-- | src/testing/test_merchant_api.c | 6 | ||||
-rw-r--r-- | src/testing/testing_api_cmd_claim_order.c | 1 | ||||
-rw-r--r-- | src/testing/testing_api_cmd_pay_order.c | 337 |
9 files changed, 676 insertions, 148 deletions
diff --git a/src/backend/taler-merchant-httpd_contract.c b/src/backend/taler-merchant-httpd_contract.c index f05c97d5..e7c7ac49 100644 --- a/src/backend/taler-merchant-httpd_contract.c +++ b/src/backend/taler-merchant-httpd_contract.c @@ -308,8 +308,9 @@ parse_token_families (void *cls, struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_array_const ("keys", &keys), - GNUNET_JSON_spec_bool ("critical", - &family.critical), + GNUNET_JSON_spec_bool ("critical", + &family.critical), + GNUNET_JSON_spec_end () /* TODO: Figure out if these fields should be 'const' */ // GNUNET_JSON_spec_string ("description", // &family.description), @@ -415,4 +416,43 @@ TALER_JSON_spec_token_families (const char *name, }; return ret; +} + +enum GNUNET_GenericReturnValue +TMH_find_token_family_key (const char *slug, + struct GNUNET_TIME_Timestamp valid_after, + struct TALER_MerchantContractTokenFamily *families, + unsigned int families_len, + struct TALER_MerchantContractTokenFamily *family, + struct TALER_MerchantContractTokenFamilyKey *key) +{ + for (unsigned int i = 0; i < families_len; i++) + { + if (0 != strcmp (families[i].slug, slug)) + { + continue; + } + if (NULL != family) + { + *family = families[i]; + } + for (unsigned int k = 0; k < family->keys_len; k++) + { + if (GNUNET_TIME_timestamp_cmp(family->keys[k].valid_after, + ==, + valid_after)) + { + if (NULL != key) + { + *key = family->keys[k]; + } + return GNUNET_OK; + } + } + /* matching family found, but no key. */ + return GNUNET_NO; + } + + /* no matching family found */ + return GNUNET_NO; }
\ No newline at end of file diff --git a/src/backend/taler-merchant-httpd_contract.h b/src/backend/taler-merchant-httpd_contract.h index 6c1841e9..006d0205 100644 --- a/src/backend/taler-merchant-httpd_contract.h +++ b/src/backend/taler-merchant-httpd_contract.h @@ -19,6 +19,7 @@ * @author Christian Blättler */ #include "taler-merchant-httpd.h" +#include <gnunet/gnunet_common.h> #include <gnunet/gnunet_time_lib.h> #include <jansson.h> @@ -614,3 +615,24 @@ struct GNUNET_JSON_Specification TALER_JSON_spec_token_families (const char *name, struct TALER_MerchantContractTokenFamily **families, unsigned int *families_len); + + +/** + * Find matching token family in @a families based on @a slug. Then use + * @a valid_after to find the matching public key within it. + * + * @param slug slug of the token family + * @param valid_after start time of the validity period of the key + * @param families array of token families to search in + * @param families_len length of the @a families array + * @param[out] family found family, set to NULL to only check for existence + * @param[out] key found key, set to NULL to only check for existence + * @return #GNUNET_OK on success #GNUNET_NO if no key was found + */ +enum GNUNET_GenericReturnValue +TMH_find_token_family_key (const char *slug, + struct GNUNET_TIME_Timestamp valid_after, + struct TALER_MerchantContractTokenFamily *families, + unsigned int families_len, + struct TALER_MerchantContractTokenFamily *family, + struct TALER_MerchantContractTokenFamilyKey *key);
\ No newline at end of file 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 1808989a..0dad53db 100644 --- a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c +++ b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c @@ -60,13 +60,13 @@ * TODO: What is a good value for this? * Maximum number of tokens that we allow as inputs per transaction */ -#define MAX_TOKEN_ALLOWED_INPUT_TOKENS 128 +#define MAX_TOKEN_ALLOWED_INPUTs 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 +#define MAX_TOKEN_ALLOWED_OUTPUTs 128 /** * How often do we ask the exchange again about our @@ -287,6 +287,25 @@ struct TokenEnvelope /** + * (Blindly) signed token to be returned to the wallet. + */ +struct SignedOutputToken +{ + + /** + * Blinded token use public keys waiting to be signed. + */ + struct TALER_TokenIssueBlindSignatureP sig; + + /** + * Hash of token issue public key. + */ + struct TALER_TokenIssuePublicKeyHashP h_issue; + +}; + + +/** * Information kept during a pay request for each exchange. */ struct ExchangeGroup @@ -365,7 +384,7 @@ struct PayContext * Array with @e output_tokens_cnt signed tokens returned in * the response to the wallet. */ - struct TALER_TokenIssueBlindSignatureP *output_tokens; + struct SignedOutputToken *output_tokens; /** * Array with @e token_envelopes_cnt (blinded) token envelopes. @@ -1484,6 +1503,28 @@ phase_batch_deposits (struct PayContext *pc) /** + * Build JSON array of blindly signed token envelopes, + * to be used in the response to the wallet. + * + * @param[in,out] pc payment context to use + */ +static json_t * +build_token_sigs (struct PayContext *pc) +{ + json_t *token_sigs = json_array (); + for (unsigned int i = 0; i < pc->output_tokens_len; i++) + { + json_array_append_new (token_sigs, GNUNET_JSON_PACK ( + GNUNET_JSON_pack_blinded_sig ("blind_sig", pc->output_tokens[i].sig.signature), + GNUNET_JSON_pack_data_auto ("h_issue", &pc->output_tokens[i].h_issue) + )); + } + + return token_sigs; +} + + +/** * Generate response (payment successful) * * @param[in,out] pc payment context where the payment was successful @@ -1493,6 +1534,7 @@ phase_success_response (struct PayContext *pc) { struct TALER_MerchantSignatureP sig; char *pos_confirmation; + json_t *token_sigs; /* Sign on our end (as the payment did go through, even if it may have been refunded already) */ @@ -1506,6 +1548,9 @@ phase_success_response (struct PayContext *pc) pc->pos_alg, &pc->amount, pc->timestamp); + token_sigs = (0 >= pc->output_tokens_len) + ? NULL + : build_token_sigs (pc); pay_end (pc, TALER_MHD_REPLY_JSON_PACK ( pc->connection, @@ -1513,6 +1558,9 @@ phase_success_response (struct PayContext *pc) GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("pos_confirmation", pos_confirmation)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_steal ("token_sigs", + token_sigs)), GNUNET_JSON_pack_data_auto ("sig", &sig))); GNUNET_free (pos_confirmation); @@ -2223,13 +2271,13 @@ find_valid_input_tokens (struct PayContext *pc, return GNUNET_NO; } - if (GNUNET_OK != TALER_merchant_token_issue_verify (&tuc->pub, - &key->pub, - &tuc->unblinded_sig)) + if (GNUNET_OK != TALER_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", + "`%s' has invalid issue signature\n", GNUNET_TIME_timestamp2s (key->valid_after)); GNUNET_break (0); pay_end (pc, @@ -2299,9 +2347,11 @@ sign_token_envelopes (struct PayContext *pc, continue; } - TALER_merchant_token_issue_sign (&pc->token_envelopes[i].blinded_token, - priv, - &pc->output_tokens[i]); + TALER_token_issue_sign (priv, + &pc->token_envelopes[i].blinded_token, + &pc->output_tokens[i].sig); + + pc->output_tokens[i].h_issue.hash = pc->token_envelopes[i].h_issue.hash; num_signed++; } @@ -2310,7 +2360,7 @@ sign_token_envelopes (struct PayContext *pc, { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Expected %d token envelopes for public key with valid_after " - "`%s', but found %d\n", + "'%s', but found %d\n", expected_num, GNUNET_TIME_timestamp2s (key->valid_after), num_signed); @@ -2328,45 +2378,6 @@ sign_token_envelopes (struct PayContext *pc, /** - * 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. @@ -2428,8 +2439,8 @@ phase_validate_tokens (struct PayContext *pc) for (unsigned int i = 0; i<selected.inputs_len; i++) { struct TALER_MerchantContractInput input = selected.inputs[i]; - struct TALER_MerchantContractTokenFamily *family = NULL; - struct TALER_MerchantContractTokenFamilyKey *key = NULL; + struct TALER_MerchantContractTokenFamily family; + struct TALER_MerchantContractTokenFamilyKey key; if (input.type != TALER_MCIT_TOKEN) { @@ -2437,13 +2448,12 @@ phase_validate_tokens (struct PayContext *pc) continue; } - lookup_token_family_key (pc, - input.details.token.token_family_slug, - input.details.token.valid_after, - family, - key); - - if (NULL == family || NULL == key) + if (GNUNET_OK != TMH_find_token_family_key (input.details.token.token_family_slug, + input.details.token.valid_after, + pc->token_families, + pc->token_families_len, + &family, + &key)) { /* this should never happen, since the choices and token families are validated on insert. */ @@ -2457,8 +2467,8 @@ phase_validate_tokens (struct PayContext *pc) } if (GNUNET_NO == find_valid_input_tokens (pc, - key, - input.details.token.count)) + &key, + input.details.token.count)) { /* Error is already scheduled from find_valid_input_token. */ return; @@ -2474,8 +2484,8 @@ phase_validate_tokens (struct PayContext *pc) 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; + struct TALER_MerchantContractTokenFamily family; + struct TALER_MerchantContractTokenFamilyKey key; if (output.type != TALER_MCOT_TOKEN) { @@ -2483,13 +2493,12 @@ phase_validate_tokens (struct PayContext *pc) continue; } - lookup_token_family_key (pc, - output.details.token.token_family_slug, - output.details.token.valid_after, - family, - key); - - if (NULL == family || NULL == key) + if (GNUNET_OK != TMH_find_token_family_key (output.details.token.token_family_slug, + output.details.token.valid_after, + pc->token_families, + pc->token_families_len, + &family, + &key)) { /* this should never happen, since the choices and token families are validated on insert. */ @@ -2504,25 +2513,27 @@ phase_validate_tokens (struct PayContext *pc) qs = TMH_db->lookup_token_family_key (TMH_db->cls, pc->hc->instance->settings.id, - family->slug, - key->valid_after, - key->valid_after, + 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)); + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL)); return; } + GNUNET_assert (NULL != details.priv.private_key); + if (GNUNET_OK != sign_token_envelopes (pc, - family->slug, - key, + family.slug, + &key, &details.priv, output.details.token.count)) { @@ -2940,10 +2951,11 @@ phase_parse_wallet_data (struct PayContext *pc) GNUNET_JSON_spec_int64 ("choice_index", &pc->choice_index), NULL), - GNUNET_JSON_spec_mark_optional( - GNUNET_JSON_spec_fixed_auto ("h_outputs", - &pc->choice_index), - NULL), + /* TODO: Add h_outputs to wallet_data */ + // GNUNET_JSON_spec_mark_optional( + // GNUNET_JSON_spec_fixed_auto ("h_outputs", + // &pc->h_outputs), + // NULL), GNUNET_JSON_spec_end () }; @@ -3032,6 +3044,7 @@ phase_parse_pay (struct PayContext *pc) /* use empty string as default if client didn't specify it */ pc->session_id = GNUNET_strdup (""); } + pc->coins_cnt = json_array_size (coins); if (pc->coins_cnt > MAX_COIN_ALLOWED_COINS) { @@ -3044,30 +3057,10 @@ phase_parse_pay (struct PayContext *pc) "'coins' array too long")); return; } - - pc->tokens_cnt = json_array_size (tokens); - if (pc->tokens_cnt > MAX_TOKEN_ALLOWED_INPUT_TOKENS) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "'tokens' array too long")); - return; - } - /* note: 1 coin = 1 deposit confirmation expected */ pc->dc = GNUNET_new_array (pc->coins_cnt, struct DepositConfirmation); - 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; @@ -3183,6 +3176,22 @@ phase_parse_pay (struct PayContext *pc) } } + pc->tokens_cnt = json_array_size (tokens); + if (pc->tokens_cnt > MAX_TOKEN_ALLOWED_INPUTs) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "'tokens' array too long")); + return; + } + + pc->tokens = GNUNET_new_array (pc->tokens_cnt, + struct TokenUseConfirmation); + /* This look populates the array 'tokens' in 'pc' */ { unsigned int tokens_index; @@ -3235,6 +3244,22 @@ phase_parse_pay (struct PayContext *pc) } } + pc->token_envelopes_cnt = json_array_size (tokens_evs); + if (pc->token_envelopes_cnt > MAX_TOKEN_ALLOWED_OUTPUTs) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "'token_evs' array too long")); + return; + } + + pc->token_envelopes = GNUNET_new_array (pc->token_envelopes_cnt, + struct TokenEnvelope); + { unsigned int tokens_ev_index; json_t *token_ev; @@ -3246,7 +3271,9 @@ phase_parse_pay (struct PayContext *pc) TALER_JSON_spec_token_envelope ("token_ev", &ev->blinded_token), GNUNET_JSON_spec_fixed_auto ("h_issue", - &ev->h_issue.hash)}; + &ev->h_issue.hash), + GNUNET_JSON_spec_end () + }; enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_json_data (pc->connection, diff --git a/src/backenddb/pg_lookup_token_family_key.c b/src/backenddb/pg_lookup_token_family_key.c index 33fc0b27..a0a5fab1 100644 --- a/src/backenddb/pg_lookup_token_family_key.c +++ b/src/backenddb/pg_lookup_token_family_key.c @@ -131,11 +131,12 @@ TMH_PG_lookup_token_family_key (void *cls, " LEFT JOIN merchant_token_family_keys" " ON merchant_token_families.token_family_serial = merchant_token_family_keys.token_family_serial" " AND merchant_token_family_keys.valid_after >= $3" - " AND merchant_token_family_keys.valid_after < $4" + " AND merchant_token_family_keys.valid_after <= $4" /* TODO: Should this be < instead of <=? */ " JOIN merchant_instances" " USING (merchant_serial)" " WHERE merchant_instances.merchant_id=$1" " AND slug=$2" + " ORDER BY merchant_token_family_keys.valid_after ASC" " LIMIT 1"); enum GNUNET_DB_QueryStatus qs; qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, @@ -161,4 +162,4 @@ TMH_PG_lookup_token_family_key (void *cls, } return qs; -}
\ No newline at end of file +} diff --git a/src/include/taler_merchant_service.h b/src/include/taler_merchant_service.h index e3d65188..1907f88f 100644 --- a/src/include/taler_merchant_service.h +++ b/src/include/taler_merchant_service.h @@ -3091,20 +3091,41 @@ TALER_MERCHANT_order_claim_cancel (struct TALER_MERCHANT_OrderClaimHandle *och); */ struct TALER_MERCHANT_PrivateTokenDetails { + + /** + * Master secret used to derive the private key from. + */ + struct TALER_TokenUseMasterSecretP master; + /** * Private key of the token. */ struct TALER_TokenUsePrivateKeyP token_priv; /** + * Public key of the token. + */ + struct TALER_TokenUsePublicKeyP token_pub; + + /** + * Public key of the token. + */ + struct TALER_TokenUsePublicKeyHashP h_token_pub; + + /** + * Blinded public key of the token. + */ + struct TALER_TokenEnvelopeP envelope; + + /** * Value used to blind the key for the signature. */ union GNUNET_CRYPTO_BlindingSecretP blinding_secret; /** - * Unblinded token issue signature made by the merchant. + * Inputs needed from the merchant for blind signing. */ - struct TALER_TokenIssueSignatureP issue_sig; + struct TALER_TokenUseMerchantValues blinding_inputs; /** * Token issue public key. @@ -3112,9 +3133,14 @@ struct TALER_MERCHANT_PrivateTokenDetails struct TALER_TokenIssuePublicKeyP issue_pub; /** - * Inputs needed from the merchant for blind signing. + * Unblinded token issue signature made by the merchant. */ - struct GNUNET_CRYPTO_BlindingInputValues *blinding_inputs; + struct TALER_TokenIssueSignatureP issue_sig; + + /** + * Blinded token issue signature made by the merchant. + */ + struct TALER_TokenIssueBlindSignatureP blinded_sig; }; @@ -3164,7 +3190,7 @@ struct TALER_MERCHANT_PayResponse /** * Array of tokens that were issued for the payment. */ - struct TALER_MERCHANT_PrivateTokenDetails *tokens; + struct TALER_MERCHANT_OutputToken *tokens; /** * Length of the @e tokens array. @@ -3276,6 +3302,32 @@ struct TALER_MERCHANT_UsedToken /** + * Information the frontend forwards to the backend for an output token. The + * blinded issue signature is set once the request return with an HTTP_OK. + * This does not inlcude the blinding secret or any other private information. + */ +struct TALER_MERCHANT_OutputToken +{ + + /** + * Token envelope. + */ + struct TALER_TokenEnvelopeP envelope; + + /** + * Hash of the corresponding token issue public key. + */ + struct TALER_TokenIssuePublicKeyHashP h_issue; + + /** + * Blinded issue signature made by the merchant. + */ + struct TALER_TokenIssueBlindSignatureP blinded_sig; + +}; + + +/** * Pay a merchant. API for frontends talking to backends. Here, * the frontend does not have the coin's private keys, but just * the public keys and signatures. @@ -3292,6 +3344,8 @@ struct TALER_MERCHANT_UsedToken * @param coins array of coins to pay with * @param num_tokens length of the @a tokens array * @param tokens array of tokens used + * @param num_output_tokens length of the @a output_tokens array + * @param output_tokens array of output token to be issued by the merchant * @param pay_cb the callback to call when a reply for this request is available * @param pay_cb_cls closure for @a pay_cb * @return a handle for this request @@ -3307,6 +3361,8 @@ TALER_MERCHANT_order_pay_frontend ( const struct TALER_MERCHANT_PaidCoin coins[static num_coins], unsigned int num_tokens, const struct TALER_MERCHANT_UsedToken tokens[static num_tokens], + unsigned int num_output_tokens, + const struct TALER_MERCHANT_OutputToken output_tokens[static num_output_tokens], TALER_MERCHANT_OrderPayCallback pay_cb, void *pay_cb_cls); @@ -3407,6 +3463,8 @@ struct TALER_MERCHANT_UseToken * @param coins array of coins we use to pay * @param num_tokens number of tokens to used in this payment request * @param tokens array of tokens we use in this payment request + * @param num_output_tokens length of the @a output_tokens array + * @param output_tokens array of output tokens to be issued by the merchant * @param pay_cb the callback to call when a reply for this request is available * @param pay_cb_cls closure for @a pay_cb * @return a handle for this request @@ -3431,6 +3489,8 @@ TALER_MERCHANT_order_pay ( const struct TALER_MERCHANT_PayCoin coins[static num_coins], unsigned int num_tokens, const struct TALER_MERCHANT_UseToken tokens[static num_tokens], + unsigned int num_output_tokens, + const struct TALER_MERCHANT_OutputToken output_tokens[static num_output_tokens], TALER_MERCHANT_OrderPayCallback pay_cb, void *pay_cb_cls); diff --git a/src/lib/merchant_api_post_order_pay.c b/src/lib/merchant_api_post_order_pay.c index c74113b6..39e4e35b 100644 --- a/src/lib/merchant_api_post_order_pay.c +++ b/src/lib/merchant_api_post_order_pay.c @@ -34,6 +34,7 @@ #include "taler_merchant_service.h" #include "merchant_api_common.h" #include "merchant_api_curl_defaults.h" +#include <stdio.h> #include <taler/taler_json_lib.h> #include <taler/taler_signatures.h> #include <taler/taler_exchange_service.h> @@ -82,11 +83,6 @@ struct TALER_MERCHANT_OrderPayHandle struct TALER_MERCHANT_PaidCoin *coins; /** - * The tokens we are using as inputs. - */ - struct TALER_MERCHANT_UsedToken *tokens; - - /** * Hash of the contract we are paying, set * if @e am_wallet is true. */ @@ -121,11 +117,6 @@ struct TALER_MERCHANT_OrderPayHandle unsigned int num_coins; /** - * Number of @e tokens we are using. - */ - unsigned int num_tokens; - - /** * Set to true if this is the wallet API and we have * initialized @e h_contract_terms and @e merchant_pub. */ @@ -136,10 +127,14 @@ struct TALER_MERCHANT_OrderPayHandle /** * Parse blindly signed output tokens from response. + * + * @param token_sigs the JSON array with the token signatures. Can be NULL. + * @param tokens where to store the parsed tokens. + * @param num_tokens where to store the length of the @a tokens array. */ static enum GNUNET_GenericReturnValue parse_tokens (const json_t *token_sigs, - struct TALER_MERCHANT_PrivateTokenDetails **tokens, + struct TALER_MERCHANT_OutputToken **tokens, unsigned int *num_tokens) { GNUNET_array_grow (*tokens, @@ -148,22 +143,25 @@ parse_tokens (const json_t *token_sigs, for (unsigned int i = 0; i<(*num_tokens); i++) { - struct TALER_MERCHANT_PrivateTokenDetails *token = &(*tokens)[i]; - const json_t *js = json_array_get (token_sigs, + struct TALER_MERCHANT_OutputToken *token = &(*tokens)[i]; + const json_t *jtoken = json_array_get (token_sigs, i); - if (NULL == js) + if (NULL == jtoken) { GNUNET_break (0); return GNUNET_SYSERR; } struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_token_issue_sig ("blind_sig", - &token->issue_sig), + TALER_JSON_spec_blinded_token_issue_sig ("blind_sig", + &token->blinded_sig), + GNUNET_JSON_spec_fixed_auto ("h_issue", + &token->h_issue), + GNUNET_JSON_spec_end () }; if (GNUNET_OK != - GNUNET_JSON_parse (js, + GNUNET_JSON_parse (jtoken, spec, NULL, NULL)) { @@ -172,7 +170,7 @@ parse_tokens (const json_t *token_sigs, } } - return GNUNET_NO; + return GNUNET_YES; } @@ -196,6 +194,14 @@ handle_pay_finished (void *cls, .hr.reply = json }; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Received /pay response with status code %u\n", + (unsigned int) response_code); + + json_dumpf (json, + stderr, + JSON_INDENT (2)); + oph->job = NULL; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "/pay completed with response code %u\n", @@ -235,19 +241,16 @@ handle_pay_finished (void *cls, break; } - if (NULL != token_sigs) + if (GNUNET_OK != + parse_tokens (token_sigs, + &pr.details.ok.tokens, + &pr.details.ok.num_tokens)) { - if (GNUNET_OK != - parse_tokens (token_sigs, - &pr.details.ok.tokens, - &pr.details.ok.num_tokens)) - { - GNUNET_break_op (0); - pr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; - pr.hr.http_status = 0; - pr.hr.hint = "failed to parse token_sigs field in response"; - break; - } + GNUNET_break_op (0); + pr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + pr.hr.http_status = 0; + pr.hr.hint = "failed to parse token_sigs field in response"; + break; } if (GNUNET_OK != @@ -381,6 +384,8 @@ TALER_MERCHANT_order_pay_frontend ( const struct TALER_MERCHANT_PaidCoin coins[static num_coins], unsigned int num_tokens, const struct TALER_MERCHANT_UsedToken tokens[static num_tokens], + unsigned int num_output_tokens, + const struct TALER_MERCHANT_OutputToken output_tokens[static num_output_tokens], TALER_MERCHANT_OrderPayCallback pay_cb, void *pay_cb_cls) { @@ -388,6 +393,7 @@ TALER_MERCHANT_order_pay_frontend ( json_t *pay_obj; json_t *j_coins; json_t *j_tokens = NULL; + json_t *j_output_tokens = NULL; CURL *eh; struct TALER_Amount total_fee; struct TALER_Amount total_amount; @@ -496,6 +502,31 @@ TALER_MERCHANT_order_pay_frontend ( } } + if (0 < num_output_tokens) + { + j_output_tokens = json_array (); + GNUNET_assert (NULL != j_output_tokens); + for (unsigned int i = 0; i<num_output_tokens; i++) + { + json_t *j_token_ev; + const struct TALER_MERCHANT_OutputToken *ev = &output_tokens[i]; + + j_token_ev = GNUNET_JSON_PACK ( + TALER_JSON_pack_token_envelope ("token_ev", + &ev->envelope), + GNUNET_JSON_pack_data_auto ("h_issue", + &ev->h_issue.hash)); + if (0 != + json_array_append_new (j_output_tokens, + j_token_ev)) + { + GNUNET_break (0); + json_decref (j_output_tokens); + return NULL; + } + } + } + pay_obj = GNUNET_JSON_PACK ( GNUNET_JSON_pack_array_steal ("coins", j_coins), @@ -503,12 +534,19 @@ TALER_MERCHANT_order_pay_frontend ( GNUNET_JSON_pack_array_steal ("tokens", j_tokens)), GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_steal ("tokens_evs", + j_output_tokens)), + GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_object_incref ("wallet_data", (json_t *) wallet_data)), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("session_id", session_id))); + json_dumpf (pay_obj, + stderr, + JSON_INDENT (2)); + oph = GNUNET_new (struct TALER_MERCHANT_OrderPayHandle); oph->ctx = ctx; oph->pay_cb = pay_cb; @@ -538,6 +576,8 @@ TALER_MERCHANT_order_pay_frontend ( GNUNET_memcpy (oph->coins, coins, num_coins * sizeof (struct TALER_MERCHANT_PaidCoin)); + /* TODO: Copy token_evs to pay handle so they + can be unblinded in the callback. */ eh = TALER_MERCHANT_curl_easy_get_ (oph->url); if (GNUNET_OK != @@ -582,6 +622,8 @@ TALER_MERCHANT_order_pay ( const struct TALER_MERCHANT_PayCoin coins[static num_coins], unsigned int num_tokens, const struct TALER_MERCHANT_UseToken tokens[static num_tokens], + unsigned int num_output_tokens, + const struct TALER_MERCHANT_OutputToken output_tokens[static num_output_tokens], TALER_MERCHANT_OrderPayCallback pay_cb, void *pay_cb_cls) { @@ -678,6 +720,8 @@ TALER_MERCHANT_order_pay ( pc, num_tokens, ut, + num_output_tokens, + output_tokens, pay_cb, pay_cb_cls); if (NULL == oph) diff --git a/src/testing/test_merchant_api.c b/src/testing/test_merchant_api.c index 9f687588..9e9025ed 100644 --- a/src/testing/test_merchant_api.c +++ b/src/testing/test_merchant_api.c @@ -1725,11 +1725,11 @@ run (void *cls, GNUNET_TIME_UNIT_ZERO_TS, GNUNET_TIME_UNIT_FOREVER_TS, "EUR:0.0"), - TALER_TESTING_cmd_merchant_pay_order_choices ("pay-order-with-output", + TALER_TESTING_cmd_merchant_pay_order_choices ("pay-order-with-input-and-output", merchant_url, MHD_HTTP_OK, - "create-order-with-output", - "withdraw-coin-1", + "create-order-with-input-and-output", + "withdraw-coin-2", "EUR:5", "EUR:4.99", NULL, diff --git a/src/testing/testing_api_cmd_claim_order.c b/src/testing/testing_api_cmd_claim_order.c index aec03876..353cd914 100644 --- a/src/testing/testing_api_cmd_claim_order.c +++ b/src/testing/testing_api_cmd_claim_order.c @@ -246,6 +246,7 @@ order_claim_traits (void *cls, unsigned int index) { struct OrderClaimState *pls = cls; + struct TALER_TESTING_Trait traits[] = { TALER_TESTING_make_trait_contract_terms (pls->contract_terms), TALER_TESTING_make_trait_h_contract_terms (&pls->contract_terms_hash), diff --git a/src/testing/testing_api_cmd_pay_order.c b/src/testing/testing_api_cmd_pay_order.c index 12653ebc..143540e9 100644 --- a/src/testing/testing_api_cmd_pay_order.c +++ b/src/testing/testing_api_cmd_pay_order.c @@ -24,7 +24,11 @@ */ #include "platform.h" #include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_time_lib.h> #include <jansson.h> +#include <stddef.h> +#include <stdint.h> #include <taler/taler_exchange_service.h> #include <taler/taler_testing_lib.h> #include <taler/taler_signatures.h> @@ -131,10 +135,132 @@ struct PayState */ const json_t *wallet_data; + /** + * Index of the choice to be used in the payment. -1 for orders without choices. + */ + int choice_index; + }; /** + * Find the token issue public key for a given token family @a slug and + * @a valid_after timestamp. + * + * @param token_families json object of token families where the key is the slug + * @param slug the slug of the token family + * @param valid_after the timestamp of the token family + * @param[out] pub the token issue public key of the token family + * @return #GNUNET_OK on success and #GNUNET_SYSERR if not found + */ +static enum GNUNET_GenericReturnValue +find_token_public_key (const json_t *token_families, + const char *slug, + struct GNUNET_TIME_Timestamp valid_after, + struct TALER_TokenIssuePublicKeyP *pub) +{ + const json_t *tf = json_object_get (token_families, slug); + const json_t *keys; + + if (NULL == tf) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Token family `%s' not found\n", + slug); + return GNUNET_SYSERR; + } + + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("keys", + &keys), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (tf, + spec, + NULL, + NULL)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse token family `%s'\n", + slug); + return GNUNET_SYSERR; + } + + { + unsigned int i; + const json_t *key; + + json_array_foreach (keys, i, key) + { + int64_t cipher; + struct GNUNET_TIME_Timestamp ivalid_after; + struct GNUNET_CRYPTO_BlindSignPublicKey *issue_pub = GNUNET_new (struct GNUNET_CRYPTO_BlindSignPublicKey); + const char *error_name; + unsigned int error_line; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_fixed_auto ("h_pub", + &issue_pub->pub_key_hash), + GNUNET_JSON_spec_rsa_public_key ("rsa_pub", + &issue_pub->details.rsa_public_key), + // GNUNET_JSON_spec_fixed_auto ("cs_pub", + // &key.pub.public_key->details.cs_public_key)), + GNUNET_JSON_spec_int64 ("cipher", + &cipher), + GNUNET_JSON_spec_timestamp ("valid_after", + &ivalid_after), + GNUNET_JSON_spec_end() + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (key, + ispec, + &error_name, + &error_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse %s at %u: %s\n", + ispec[error_line].field, + error_line, + error_name); + return GNUNET_SYSERR; + } + + switch (cipher) { + case GNUNET_CRYPTO_BSA_RSA: + issue_pub->cipher = GNUNET_CRYPTO_BSA_RSA; + break; + case GNUNET_CRYPTO_BSA_CS: + issue_pub->cipher = GNUNET_CRYPTO_BSA_CS; + break; + case GNUNET_CRYPTO_BSA_INVALID: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Field 'cipher' invalid in key #%u\n", + i); + return GNUNET_SYSERR; + } + + /* Compare valid_after to make sure it matches. */ + if (GNUNET_TIME_timestamp_cmp(valid_after, !=, ivalid_after)) + { + continue; + } + + pub->public_key = issue_pub; + return GNUNET_OK; + } + } + + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Key with valid_after '%s' for token family '%s' not found\n", + GNUNET_TIME_timestamp2s(valid_after), + slug); + return GNUNET_SYSERR; +} + + +/** * Parse the @a coins specification and grow the @a pc * array with the coins found, updating @a npc. * @@ -376,8 +502,55 @@ pay_cb (void *cls, if (MHD_HTTP_OK == pr->hr.http_status) { ps->merchant_sig = pr->details.ok.merchant_sig; - ps->issued_tokens = pr->details.ok.tokens; - ps->num_issued_tokens = pr->details.ok.num_tokens; + if (ps->num_issued_tokens != pr->details.ok.num_tokens) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected number of tokens issued. " + "Sent %d envelopes but got %d tokens issued.\n", + ps->num_issued_tokens, + pr->details.ok.num_tokens); + GNUNET_break (0); + TALER_TESTING_interpreter_fail (ps->is); + return; + } + for (unsigned int i = 0; i < ps->num_issued_tokens; i++) + { + struct TALER_MERCHANT_PrivateTokenDetails *details = + &ps->issued_tokens[i]; + /* The issued tokens should be in the + same order as the provided envelopes. */ + if (1 == GNUNET_CRYPTO_hash_cmp( + &details->issue_pub.public_key->pub_key_hash, + &pr->details.ok.tokens[i].h_issue.hash)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected token issued. " + "Expected token with hash %s but got token with hash %s.\n", + GNUNET_h2s (&details->issue_pub.public_key->pub_key_hash), + GNUNET_h2s (&pr->details.ok.tokens[i].h_issue.hash)); + GNUNET_break (0); + TALER_TESTING_interpreter_fail (ps->is); + return; + } + + ps->issued_tokens[i].blinded_sig = pr->details.ok.tokens[i].blinded_sig; + + + if (GNUNET_OK != + TALER_token_issue_sig_unblind (&details->issue_sig, + &details->blinded_sig, + &details->blinding_secret, + &details->h_token_pub, + &details->blinding_inputs, + &details->issue_pub)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to unblind token signature\n"); + GNUNET_break (0); + TALER_TESTING_interpreter_fail (ps->is); + return; + } + } if (NULL != ps->pos_key) { char *pc; @@ -442,12 +615,16 @@ pay_run (void *cls, struct TALER_MerchantWireHashP h_wire; const struct TALER_PrivateContractHashP *h_proposal; struct TALER_Amount max_fee; + const json_t *choices = NULL; + const json_t *token_families = NULL; const char *error_name = NULL; unsigned int error_line = 0; struct TALER_MERCHANT_PayCoin *pay_coins; unsigned int npay_coins; struct TALER_MERCHANT_UseToken *use_tokens = NULL; unsigned int len_use_tokens = 0; + struct TALER_MERCHANT_OutputToken *output_tokens = NULL; + unsigned int len_output_tokens = 0; const struct TALER_MerchantSignatureP *merchant_sig; const enum TALER_MerchantConfirmationAlgorithm *alg_ptr; @@ -494,6 +671,14 @@ pay_run (void *cls, &ps->total_amount), TALER_JSON_spec_amount_any ("max_fee", &max_fee), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("token_families", + &token_families), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("choices", + &choices), + NULL), /* FIXME oec: parse minimum age, use data later? */ GNUNET_JSON_spec_end () }; @@ -559,6 +744,151 @@ pay_run (void *cls, } GNUNET_free (tr); } + if (0 <= ps->choice_index) + { + const json_t *outputs; + json_t *output; + unsigned int output_index; + const json_t *choice = json_array_get (choices, ps->choice_index); + + if (NULL == choice) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "No choice found at index %d\n", + ps->choice_index); + TALER_TESTING_FAIL (is); + } + + { + const char *ierror_name = NULL; + unsigned int ierror_line = 0; + + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_array_const ("outputs", + &outputs), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (choice, + ispec, + &ierror_name, + &ierror_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Parser failed on %s:%u for input `%s'\n", + ierror_name, + ierror_line, + json_dumps (choice, + JSON_INDENT (2))); + TALER_TESTING_FAIL (is); + } + } + + json_array_foreach (outputs, output_index, output) + { + const char *slug; + const char *kind; + struct GNUNET_TIME_Timestamp valid_after; + uint32_t count = 1; + const char *ierror_name = NULL; + unsigned int ierror_line = 0; + + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_string ("kind", + &kind), + GNUNET_JSON_spec_string ("token_family_slug", + &slug), + GNUNET_JSON_spec_timestamp ("valid_after", + &valid_after), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("count", + &count), + NULL), + GNUNET_JSON_spec_end() + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (output, + ispec, + &ierror_name, + &ierror_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Parser failed on %s:%u for input `%s'\n", + ierror_name, + ierror_line, + json_dumps (output, + JSON_INDENT (2))); + TALER_TESTING_FAIL (is); + } + + if (0 != strcmp("token", kind)) + { + continue; + } + + GNUNET_array_grow (ps->issued_tokens, + ps->num_issued_tokens, + ps->num_issued_tokens + count); + + for (unsigned int k = 0; k < count; k++) + { + struct TALER_MERCHANT_PrivateTokenDetails *details = + &ps->issued_tokens[ps->num_issued_tokens - count + k]; + + if (GNUNET_OK != find_token_public_key (token_families, + slug, + valid_after, + &details->issue_pub)) + { + TALER_TESTING_FAIL (is); + } + + /* Only RSA is supported for now. */ + GNUNET_assert (GNUNET_CRYPTO_BSA_RSA == details->issue_pub.public_key->cipher); + + TALER_token_blind_input_copy (&details->blinding_inputs, + TALER_token_bling_input_rsa_singleton ()); + /* TODO: Where to get details->blinding_inputs from? */ + TALER_token_use_setup_random (&details->master); + TALER_token_use_setup_priv (&details->master, + &details->blinding_inputs, + &details->token_priv); + TALER_token_use_blinding_secret_create (&details->master, + &details->blinding_inputs, + &details->blinding_secret); + GNUNET_CRYPTO_eddsa_key_get_public (&details->token_priv.private_key, + &details->token_pub.public_key); + GNUNET_CRYPTO_hash (&details->token_pub.public_key, + sizeof (struct GNUNET_CRYPTO_EcdsaPublicKey), + &details->h_token_pub.hash); + details->envelope.blinded_pub = GNUNET_CRYPTO_message_blind_to_sign ( + details->issue_pub.public_key, + &details->blinding_secret, + NULL, /* TODO: Add session nonce to support CS tokens */ + &details->h_token_pub.hash, + sizeof (details->h_token_pub.hash), + details->blinding_inputs.blinding_inputs); + + if (NULL == details->envelope.blinded_pub) + { + GNUNET_break (0); + TALER_TESTING_FAIL (is); + } + } + } + } + + GNUNET_array_grow (output_tokens, + len_output_tokens, + ps->num_issued_tokens); + for (unsigned int i = 0; i<len_output_tokens; i++) + { + output_tokens[i].h_issue.hash = ps->issued_tokens[i].issue_pub.public_key->pub_key_hash; + output_tokens[i].envelope.blinded_pub = ps->issued_tokens[i].envelope.blinded_pub; + } + if (GNUNET_OK != TALER_TESTING_get_trait_merchant_sig (proposal_cmd, &merchant_sig)) @@ -588,6 +918,8 @@ pay_run (void *cls, pay_coins, len_use_tokens, use_tokens, + len_output_tokens, + output_tokens, &pay_cb, ps); GNUNET_array_grow (pay_coins, @@ -733,6 +1065,7 @@ TALER_TESTING_cmd_merchant_pay_order_choices (const char *label, ps->amount_without_fee = amount_without_fee; ps->session_id = session_id; ps->token_reference = token_reference; + ps->choice_index = choice_index; if (0 <= choice_index) { ps->wallet_data = json_pack ("{s:i}", |