exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

commit 03cb0a556a7856247689bb698ca9fb0a7f2f021d
parent 7330005baae937c2eaae1e76fdbd4e66881cc0c2
Author: Florian Dold <dold@taler.net>
Date:   Thu, 27 Aug 2026 16:03:03 +0200

exchange /keys: add denom filter for withdrawal

Issue: https://bugs.taler.net/n/0011750

Diffstat:
Msrc/exchange/taler-exchange-httpd_get-keys.c | 896++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Msrc/exchange/taler-exchange-httpd_get-keys.h | 6++++++
2 files changed, 716 insertions(+), 186 deletions(-)

diff --git a/src/exchange/taler-exchange-httpd_get-keys.c b/src/exchange/taler-exchange-httpd_get-keys.c @@ -213,6 +213,16 @@ struct TEH_KeyStateHandle unsigned int krd_array_length; /** + * Pre-built response for `denom_filter=withdraw`. + */ + struct KeysResponseData *withdraw_krd_array; + + /** + * Length of @e withdraw_krd_array. Either zero or one. + */ + unsigned int withdraw_krd_array_length; + + /** * Cached reply for a GET /management/keys request. Used so we do not * re-create the reply every time. */ @@ -242,6 +252,11 @@ struct TEH_KeyStateHandle struct GNUNET_TIME_Timestamp signature_expires; /** + * When the membership of the withdrawal-focused response next changes. + */ + struct GNUNET_TIME_Timestamp withdraw_expiration; + + /** * True if #finish_keys_response() was not yet run and this key state * is only suitable for the /management/keys API. */ @@ -1024,28 +1039,45 @@ TEH_resume_keys_requests (bool do_shutdown) /** - * Clear memory for responses to "/keys" in @a ksh. + * Clear a cached array of `/keys` responses. * - * @param[in,out] ksh key state to update + * @param[in,out] array response array to free + * @param[in,out] array_length length of @a array, set to zero */ static void -clear_response_cache (struct TEH_KeyStateHandle *ksh) +clear_response_array (struct KeysResponseData **array, + unsigned int *array_length) { - for (unsigned int i = 0; i<ksh->krd_array_length; i++) + for (unsigned int i = 0; i<*array_length; i++) { - struct KeysResponseData *krd = &ksh->krd_array[i]; + struct KeysResponseData *krd = &(*array)[i]; MHD_destroy_response (krd->response_compressed); MHD_destroy_response (krd->response_uncompressed); GNUNET_free (krd->etag); } - GNUNET_array_grow (ksh->krd_array, - ksh->krd_array_length, + GNUNET_array_grow (*array, + *array_length, 0); } /** + * Clear memory for responses to "/keys" in @a ksh. + * + * @param[in,out] ksh key state to update + */ +static void +clear_response_cache (struct TEH_KeyStateHandle *ksh) +{ + clear_response_array (&ksh->krd_array, + &ksh->krd_array_length); + clear_response_array (&ksh->withdraw_krd_array, + &ksh->withdraw_krd_array_length); +} + + +/** * Free denomination key data. * * @param cls a `struct TEH_KeyStateHandle`, unused @@ -1661,16 +1693,348 @@ add_denom_key_cb (void *cls, /** + * Earliest future denomination start for one withdrawal family. + */ +struct WithdrawFamily +{ + /** + * Earliest denomination start after the key-state build time. + */ + struct GNUNET_TIME_Timestamp next_start; +}; + + +/** + * Context used while finding future denomination starts. + */ +struct FindWithdrawFamiliesContext +{ + /** + * Exchange time used consistently for this key-state build. + */ + struct GNUNET_TIME_Timestamp now; + + /** + * Map from family identifiers to `struct WithdrawFamily`. + */ + struct GNUNET_CONTAINER_MultiHashMap *families; +}; + + +/** + * Context used while marking withdrawal-relevant denominations. + */ +struct SelectWithdrawDenominationsContext +{ + /** + * Exchange time used consistently for this key-state build. + */ + struct GNUNET_TIME_Timestamp now; + + /** + * Map from family identifiers to `struct WithdrawFamily`. + */ + struct GNUNET_CONTAINER_MultiHashMap *families; + + /** + * Key state whose expiration must be capped at the next membership change. + */ + struct TEH_KeyStateHandle *ksh; + + /** + * Number of selected denomination keys. + */ + unsigned int selected; +}; + + +/** + * Compute the identifier for the withdrawal family of @a dk. Fees are + * deliberately zero here: changing fees creates a new response group, but + * not a separate denomination family for successor selection. + * + * @param dk denomination to identify + * @param[out] key resulting family identifier + */ +static void +get_withdraw_family_key (const struct TEH_DenominationKey *dk, + struct GNUNET_HashCode *key) +{ + struct TALER_DenominationGroup meta = { + .cipher = dk->denom_pub.bsign_pub_key->cipher, + .value = dk->meta.value, + .fees = dk->meta.fees, + .age_mask = dk->meta.age_mask, + }; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (meta.fees.withdraw.currency, + &meta.fees.withdraw)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (meta.fees.deposit.currency, + &meta.fees.deposit)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (meta.fees.refresh.currency, + &meta.fees.refresh)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (meta.fees.refund.currency, + &meta.fees.refund)); + TALER_denomination_group_get_key (&meta, + key); +} + + +/** + * Find the earliest future key start for every denomination family. + * + * @param cls a `struct FindWithdrawFamiliesContext *` + * @param h_denom_pub denomination hash, unused + * @param value a `struct TEH_DenominationKey *` + * @return #GNUNET_OK to continue iteration + */ +static enum GNUNET_GenericReturnValue +find_withdraw_family_cb (void *cls, + const struct GNUNET_HashCode *h_denom_pub, + void *value) +{ + struct FindWithdrawFamiliesContext *ctx = cls; + struct TEH_DenominationKey *dk = value; + struct WithdrawFamily *family; + struct GNUNET_HashCode key; + + (void) h_denom_pub; + dk->withdraw_relevant = false; + if (dk->recoup_possible || + GNUNET_TIME_timestamp_cmp (dk->meta.start, + <=, + ctx->now)) + return GNUNET_OK; + get_withdraw_family_key (dk, + &key); + family = GNUNET_CONTAINER_multihashmap_get (ctx->families, + &key); + if (NULL == family) + { + family = GNUNET_new (struct WithdrawFamily); + family->next_start = dk->meta.start; + GNUNET_assert ( + GNUNET_OK == + GNUNET_CONTAINER_multihashmap_put ( + ctx->families, + &key, + family, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); + return GNUNET_OK; + } + family->next_start = GNUNET_TIME_timestamp_min (family->next_start, + dk->meta.start); + return GNUNET_OK; +} + + +/** + * Mark current denominations and each family's earliest future denomination. + * + * @param cls a `struct SelectWithdrawDenominationsContext *` + * @param h_denom_pub denomination hash, unused + * @param value a `struct TEH_DenominationKey *` + * @return #GNUNET_OK to continue iteration + */ +static enum GNUNET_GenericReturnValue +select_withdraw_denomination_cb (void *cls, + const struct GNUNET_HashCode *h_denom_pub, + void *value) +{ + struct SelectWithdrawDenominationsContext *ctx = cls; + struct TEH_DenominationKey *dk = value; + struct GNUNET_TIME_Timestamp transition; + bool selected = false; + + (void) h_denom_pub; + if (dk->recoup_possible) + return GNUNET_OK; + if (GNUNET_TIME_timestamp_cmp (dk->meta.start, + <=, + ctx->now) && + GNUNET_TIME_timestamp_cmp (dk->meta.expire_withdraw, + >, + ctx->now)) + { + selected = true; + transition = dk->meta.expire_withdraw; + } + else if (GNUNET_TIME_timestamp_cmp (dk->meta.start, + >, + ctx->now)) + { + struct GNUNET_HashCode key; + const struct WithdrawFamily *family; + + get_withdraw_family_key (dk, + &key); + family = GNUNET_CONTAINER_multihashmap_get (ctx->families, + &key); + GNUNET_assert (NULL != family); + if (GNUNET_TIME_timestamp_cmp (dk->meta.start, + ==, + family->next_start)) + { + selected = true; + transition = dk->meta.start; + } + } + if (! selected) + return GNUNET_OK; + dk->withdraw_relevant = true; + ctx->selected++; + ctx->ksh->withdraw_expiration = GNUNET_TIME_timestamp_min ( + ctx->ksh->withdraw_expiration, + transition); + ctx->ksh->signature_expires = GNUNET_TIME_timestamp_min ( + ctx->ksh->signature_expires, + transition); + return GNUNET_OK; +} + + +/** + * Free a `struct WithdrawFamily` map value. + * + * @param cls unused + * @param key unused + * @param value family to free + * @return #GNUNET_OK + */ +static int +free_withdraw_family (void *cls, + const struct GNUNET_HashCode *key, + void *value) +{ + (void) cls; + (void) key; + GNUNET_free (value); + return GNUNET_OK; +} + + +/** + * Select denominations for `denom_filter=withdraw`. + * + * @param[in,out] ksh key state containing the denominations + * @param now exchange time to use for selection + * @return number of selected denomination keys + */ +static unsigned int +select_withdraw_denominations (struct TEH_KeyStateHandle *ksh, + struct GNUNET_TIME_Timestamp now) +{ + struct GNUNET_CONTAINER_MultiHashMap *families; + struct FindWithdrawFamiliesContext fctx = { + .now = now, + }; + struct SelectWithdrawDenominationsContext sctx = { + .now = now, + .ksh = ksh, + }; + + families = GNUNET_CONTAINER_multihashmap_create (128, + false); + fctx.families = families; + sctx.families = families; + GNUNET_CONTAINER_multihashmap_iterate (ksh->denomkey_map, + &find_withdraw_family_cb, + &fctx); + GNUNET_CONTAINER_multihashmap_iterate (ksh->denomkey_map, + &select_withdraw_denomination_cb, + &sctx); + GNUNET_CONTAINER_multihashmap_iterate (families, + &free_withdraw_family, + NULL); + GNUNET_CONTAINER_multihashmap_destroy (families); + return sctx.selected; +} + + +/** + * Build auditor information containing signatures only for denominations in + * the withdrawal-focused response. Auditor identity metadata is retained + * even when none of its denomination signatures is selected. + * + * @param ksh key state with full auditor information + * @return newly allocated JSON array + */ +static json_t * +build_withdraw_auditors (const struct TEH_KeyStateHandle *ksh) +{ + json_t *result = json_array (); + json_t *auditor; + size_t auditor_index; + + GNUNET_assert (NULL != result); + json_array_foreach (ksh->auditors, + auditor_index, + auditor) + { + const json_t *denomination_keys; + json_t *filtered_keys = json_array (); + json_t *filtered_auditor; + json_t *denom_sig; + size_t denom_index; + + GNUNET_assert (NULL != filtered_keys); + denomination_keys = json_object_get (auditor, + "denomination_keys"); + GNUNET_assert (json_is_array (denomination_keys)); + json_array_foreach (denomination_keys, + denom_index, + denom_sig) + { + struct TALER_DenominationHashP h_denom_pub; + struct TEH_DenominationKey *dk; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("denom_pub_h", + &h_denom_pub), + GNUNET_JSON_spec_end () + }; + + GNUNET_assert (GNUNET_OK == + GNUNET_JSON_parse (denom_sig, + spec, + NULL, + NULL)); + dk = GNUNET_CONTAINER_multihashmap_get (ksh->denomkey_map, + &h_denom_pub.hash); + if ( (NULL != dk) && + dk->withdraw_relevant ) + GNUNET_assert (0 == + json_array_append (filtered_keys, + denom_sig)); + } + filtered_auditor = json_deep_copy (auditor); + GNUNET_assert (NULL != filtered_auditor); + GNUNET_assert (0 == + json_object_set_new (filtered_auditor, + "denomination_keys", + filtered_keys)); + GNUNET_assert (0 == + json_array_append_new (result, + filtered_auditor)); + } + return result; +} + + +/** * Add the headers we want to set for every /keys response. * * @param cls the key state to use * @param[in,out] response the response to modify */ static void -setup_general_response_headers (void *cls, - struct MHD_Response *response) +setup_response_headers (struct TEH_KeyStateHandle *ksh, + struct MHD_Response *response, + bool withdraw_filtered) { - struct TEH_KeyStateHandle *ksh = cls; char dat[128]; TALER_MHD_add_global_headers (response, @@ -1682,7 +2046,9 @@ setup_general_response_headers (void *cls, GNUNET_break (MHD_YES == MHD_add_response_header (response, MHD_HTTP_HEADER_CACHE_CONTROL, - "public,must-revalidate,max-age=86400") + withdraw_filtered + ? "public,must-revalidate" + : "public,must-revalidate,max-age=86400") ); if (! GNUNET_TIME_relative_is_zero (ksh->rekey_frequency)) { @@ -1705,6 +2071,9 @@ setup_general_response_headers (void *cls, we = GNUNET_TIME_absolute_to_timestamp (wire_state->cache_expiration); m = GNUNET_TIME_timestamp_min (we, km); + if (withdraw_filtered) + m = GNUNET_TIME_timestamp_min (m, + ksh->withdraw_expiration); TALER_MHD_get_date_string (m.abs_time, dat); GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -1729,6 +2098,38 @@ setup_general_response_headers (void *cls, /** + * Add headers for the legacy `/keys` response. + * + * @param cls the key state to use + * @param[in,out] response response to modify + */ +static void +setup_general_response_headers (void *cls, + struct MHD_Response *response) +{ + setup_response_headers (cls, + response, + false); +} + + +/** + * Add headers for the withdrawal-focused `/keys` response. + * + * @param cls the key state to use + * @param[in,out] response response to modify + */ +static void +setup_withdraw_response_headers (void *cls, + struct MHD_Response *response) +{ + setup_response_headers (cls, + response, + true); +} + + +/** * Initialize @a krd using the given values for @a signkeys, * @a recoup and @a denoms. * @@ -1738,6 +2139,10 @@ setup_general_response_headers (void *cls, * @param[in,out] signkeys list of sign keys to return * @param[in,out] recoup list of revoked keys to return * @param[in,out] grouped_denominations list of grouped denominations to return + * @param[in] auditors auditor information to return + * @param[in,out] krd_array response array to append to + * @param[in,out] krd_array_length length of @a krd_array + * @param withdraw_filtered true when building the withdrawal-focused response * @return #GNUNET_OK on success */ static enum GNUNET_GenericReturnValue @@ -1746,7 +2151,11 @@ create_krd (struct TEH_KeyStateHandle *ksh, struct GNUNET_TIME_Timestamp last_cherry_pick_date, json_t *signkeys, json_t *recoup, - json_t *grouped_denominations) + json_t *grouped_denominations, + json_t *auditors, + struct KeysResponseData **krd_array, + unsigned int *krd_array_length, + bool withdraw_filtered) { struct KeysResponseData krd; struct TALER_ExchangePublicKeyP exchange_pub; @@ -1765,7 +2174,7 @@ create_krd (struct TEH_KeyStateHandle *ksh, GNUNET_assert (NULL != signkeys); GNUNET_assert (NULL != recoup); GNUNET_assert (NULL != grouped_denominations); - GNUNET_assert (NULL != ksh->auditors); + GNUNET_assert (NULL != auditors); GNUNET_assert (NULL != TEH_currency); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Creating /keys at cherry pick date %s\n", @@ -1868,7 +2277,7 @@ create_krd (struct TEH_KeyStateHandle *ksh, GNUNET_JSON_pack_array_incref ("denominations", grouped_denominations), GNUNET_JSON_pack_array_incref ("auditors", - ksh->auditors), + auditors), GNUNET_JSON_pack_array_incref ("global_fees", ksh->global_fees), GNUNET_JSON_pack_timestamp ("list_issue_date", @@ -1926,8 +2335,9 @@ create_krd (struct TEH_KeyStateHandle *ksh, keys_json, MHD_RESPMEM_MUST_FREE); GNUNET_assert (NULL != krd.response_uncompressed); - setup_general_response_headers (ksh, - krd.response_uncompressed); + setup_response_headers (ksh, + krd.response_uncompressed, + withdraw_filtered); /* Information is always public, revalidate after 1 day */ GNUNET_break (MHD_YES == MHD_add_response_header (krd.response_uncompressed, @@ -1948,8 +2358,9 @@ create_krd (struct TEH_KeyStateHandle *ksh, MHD_add_response_header (krd.response_compressed, MHD_HTTP_HEADER_CONTENT_ENCODING, "deflate")) ); - setup_general_response_headers (ksh, - krd.response_compressed); + setup_response_headers (ksh, + krd.response_compressed, + withdraw_filtered); /* Information is always public, revalidate after 1 day */ GNUNET_break (MHD_YES == MHD_add_response_header (krd.response_compressed, @@ -1958,8 +2369,8 @@ create_krd (struct TEH_KeyStateHandle *ksh, krd.etag = GNUNET_strdup (etag); } krd.cherry_pick_date = last_cherry_pick_date; - GNUNET_array_append (ksh->krd_array, - ksh->krd_array_length, + GNUNET_array_append (*krd_array, + *krd_array_length, krd); return GNUNET_OK; } @@ -2155,6 +2566,144 @@ compute_msig_hash (struct SignatureContext *sig_ctx, /** + * Add one denomination to a grouped denomination response. + * + * @param[in,out] denominations_by_group map used to find existing groups + * @param[in,out] grouped_denominations JSON array of denomination groups + * @param[in,out] sig_ctx signature context for the response + * @param[in] dk denomination to add + * @param[in,out] has_age_restricted_denomination set if an age-restricted + * denomination is encountered + */ +static void +append_denomination ( + struct GNUNET_CONTAINER_MultiHashMap *denominations_by_group, + json_t *grouped_denominations, + struct SignatureContext *sig_ctx, + const struct TEH_DenominationKey *dk, + bool *has_age_restricted_denomination) +{ + struct GroupData *group; + json_t *entry; + struct GNUNET_HashCode key; + struct TALER_DenominationGroup meta = { + .cipher = dk->denom_pub.bsign_pub_key->cipher, + .value = dk->meta.value, + .fees = dk->meta.fees, + .age_mask = dk->meta.age_mask, + }; + + TALER_denomination_group_get_key (&meta, + &key); + group = GNUNET_CONTAINER_multihashmap_get (denominations_by_group, + &key); + if (NULL == group) + { + bool age_restricted = 0 != meta.age_mask.bits; + const char *cipher; + + group = GNUNET_new (struct GroupData); + switch (meta.cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + cipher = age_restricted ? "RSA+age_restricted" : "RSA"; + break; + case GNUNET_CRYPTO_BSA_CS: + cipher = age_restricted ? "CS+age_restricted" : "CS"; + break; + default: + GNUNET_assert (false); + } + group->list = json_array (); + GNUNET_assert (NULL != group->list); + group->json = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("cipher", + cipher), + GNUNET_JSON_pack_array_steal ("denoms", + group->list), + TALER_JSON_PACK_DENOM_FEES ("fee", + &meta.fees), + TALER_JSON_pack_amount ("value", + &meta.value)); + GNUNET_assert (NULL != group->json); + if (age_restricted) + { + GNUNET_assert ( + 0 == + json_object_set_new (group->json, + "age_mask", + json_integer (meta.age_mask.bits))); + *has_age_restricted_denomination = true; + } + group->group_off = json_array_size (grouped_denominations); + GNUNET_assert (0 == + json_array_append_new (grouped_denominations, + group->json)); + GNUNET_assert ( + GNUNET_OK == + GNUNET_CONTAINER_multihashmap_put ( + denominations_by_group, + &key, + group, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); + } + + { + struct GNUNET_JSON_PackSpec key_spec; + bool private_key_lost; + + private_key_lost + = TEH_SECMOD_denom_priv_check_lost (&dk->h_denom_pub); + switch (meta.cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + key_spec = GNUNET_JSON_pack_rsa_public_key ( + "rsa_pub", + dk->denom_pub.bsign_pub_key->details.rsa_public_key); + break; + case GNUNET_CRYPTO_BSA_CS: + key_spec = GNUNET_JSON_pack_data_varsize ( + "cs_pub", + &dk->denom_pub.bsign_pub_key->details.cs_public_key, + sizeof (dk->denom_pub.bsign_pub_key->details.cs_public_key)); + break; + default: + GNUNET_assert (false); + } + + entry = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("master_sig", + &dk->master_sig), + GNUNET_JSON_pack_allow_null ( + private_key_lost + ? GNUNET_JSON_pack_bool ("lost", + true) + : GNUNET_JSON_pack_string ("dummy", + NULL)), + GNUNET_JSON_pack_timestamp ("stamp_start", + dk->meta.start), + GNUNET_JSON_pack_timestamp ("stamp_expire_withdraw", + dk->meta.expire_withdraw), + GNUNET_JSON_pack_timestamp ("stamp_expire_deposit", + dk->meta.expire_deposit), + GNUNET_JSON_pack_timestamp ("stamp_expire_legal", + dk->meta.expire_legal), + key_spec); + GNUNET_assert (NULL != entry); + } + + append_signature (sig_ctx, + group->group_off, + (unsigned int) json_array_size (group->list), + &dk->master_sig); + GNUNET_assert (json_is_array (group->list)); + GNUNET_assert (0 == + json_array_append_new (group->list, + entry)); +} + + +/** * Update the "/keys" responses in @a ksh, computing the detailed replies. * * This function is to recompute all (including cherry-picked) responses we @@ -2172,11 +2721,18 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) .min_sk_frequency = GNUNET_TIME_UNIT_FOREVER_REL }; json_t *grouped_denominations = NULL; + json_t *withdraw_denominations = NULL; + json_t *withdraw_auditors = NULL; struct GNUNET_TIME_Timestamp last_cherry_pick_date; + struct GNUNET_TIME_Timestamp withdraw_list_issue_date + = GNUNET_TIME_UNIT_ZERO_TS; struct GNUNET_CONTAINER_Heap *heap; struct SignatureContext sig_ctx = { 0 }; + struct SignatureContext withdraw_sig_ctx = { 0 }; + unsigned int withdraw_denomination_count; /* Remember if we have any denomination with age restriction */ bool has_age_restricted_denomination = false; + bool withdraw_has_age_restricted_denomination = false; struct WireStateHandle *wsh; wsh = get_wire_state (); @@ -2199,6 +2755,8 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) GNUNET_assert (NULL != recoup); grouped_denominations = json_array (); GNUNET_assert (NULL != grouped_denominations); + withdraw_denominations = json_array (); + GNUNET_assert (NULL != withdraw_denominations); GNUNET_CONTAINER_multipeermap_iterate (ksh->signkey_map, &add_sign_key_cb, @@ -2225,17 +2783,25 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) = GNUNET_TIME_relative_min (dkc.min_dk_frequency, sctx.min_sk_frequency); } + withdraw_denomination_count + = select_withdraw_denominations (ksh, + ksh->reload_time); + withdraw_auditors = build_withdraw_auditors (ksh); last_cherry_pick_date = GNUNET_TIME_UNIT_ZERO_TS; { struct TEH_DenominationKey *dk; struct GNUNET_CONTAINER_MultiHashMap *denominations_by_group; + struct GNUNET_CONTAINER_MultiHashMap *withdraw_denominations_by_group; denominations_by_group = GNUNET_CONTAINER_multihashmap_create (1024, GNUNET_NO /* NO, because keys are only on the stack */ ); + withdraw_denominations_by_group + = GNUNET_CONTAINER_multihashmap_create (128, + false); /* heap = max heap, sorted by start time */ while (NULL != (dk = GNUNET_CONTAINER_heap_remove_root (heap))) { @@ -2259,7 +2825,11 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) last_cherry_pick_date, sctx.signkeys, recoup, - grouped_denominations)) + grouped_denominations, + ksh->auditors, + &ksh->krd_array, + &ksh->krd_array_length, + false)) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Failed to generate key response data for %s\n", @@ -2273,147 +2843,19 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) } last_cherry_pick_date = dk->meta.start; - /* - * Group the denominations by {cipher, value, fees, age_mask}. - * - * For each group we save the group meta-data and the list of - * denominations in this group as a json-blob in the multihashmap - * denominations_by_group. - */ - { - struct GroupData *group; - json_t *entry; - struct GNUNET_HashCode key; - struct TALER_DenominationGroup meta = { - .cipher = dk->denom_pub.bsign_pub_key->cipher, - .value = dk->meta.value, - .fees = dk->meta.fees, - .age_mask = dk->meta.age_mask, - }; - - /* Search the group/JSON-blob for the key */ - TALER_denomination_group_get_key (&meta, - &key); - group = GNUNET_CONTAINER_multihashmap_get ( - denominations_by_group, - &key); - if (NULL == group) - { - /* There is no group for this meta-data yet, so we create a new group */ - bool age_restricted = meta.age_mask.bits != 0; - const char *cipher; - - group = GNUNET_new (struct GroupData); - switch (meta.cipher) - { - case GNUNET_CRYPTO_BSA_RSA: - cipher = age_restricted ? "RSA+age_restricted" : "RSA"; - break; - case GNUNET_CRYPTO_BSA_CS: - cipher = age_restricted ? "CS+age_restricted" : "CS"; - break; - default: - GNUNET_assert (false); - } - /* Create a new array for the denominations in this group */ - group->list = json_array (); - GNUNET_assert (NULL != group->list); - group->json = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("cipher", - cipher), - GNUNET_JSON_pack_array_steal ("denoms", - group->list), - TALER_JSON_PACK_DENOM_FEES ("fee", - &meta.fees), - TALER_JSON_pack_amount ("value", - &meta.value)); - GNUNET_assert (NULL != group->json); - if (age_restricted) - { - GNUNET_assert ( - 0 == - json_object_set_new (group->json, - "age_mask", - json_integer ( - meta.age_mask.bits))); - /* Remember that we have found at least _one_ age restricted denomination */ - has_age_restricted_denomination = true; - } - group->group_off - = json_array_size (grouped_denominations); - GNUNET_assert (0 == - json_array_append_new ( + append_denomination (denominations_by_group, grouped_denominations, - group->json)); - GNUNET_assert ( - GNUNET_OK == - GNUNET_CONTAINER_multihashmap_put (denominations_by_group, - &key, - group, - GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); - } - - /* Now that we have found/created the right group, add the - denomination to the list */ - { - struct GNUNET_JSON_PackSpec key_spec; - bool private_key_lost; - - private_key_lost - = TEH_SECMOD_denom_priv_check_lost (&dk->h_denom_pub); - switch (meta.cipher) - { - case GNUNET_CRYPTO_BSA_RSA: - key_spec = - GNUNET_JSON_pack_rsa_public_key ( - "rsa_pub", - dk->denom_pub.bsign_pub_key->details.rsa_public_key); - break; - case GNUNET_CRYPTO_BSA_CS: - key_spec = - GNUNET_JSON_pack_data_varsize ( - "cs_pub", - &dk->denom_pub.bsign_pub_key->details.cs_public_key, - sizeof (dk->denom_pub.bsign_pub_key->details.cs_public_key)); - break; - default: - GNUNET_assert (false); - } - - entry = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ("master_sig", - &dk->master_sig), - GNUNET_JSON_pack_allow_null ( - private_key_lost - ? GNUNET_JSON_pack_bool ("lost", - true) - : GNUNET_JSON_pack_string ("dummy", - NULL)), - GNUNET_JSON_pack_timestamp ("stamp_start", - dk->meta.start), - GNUNET_JSON_pack_timestamp ("stamp_expire_withdraw", - dk->meta.expire_withdraw), - GNUNET_JSON_pack_timestamp ("stamp_expire_deposit", - dk->meta.expire_deposit), - GNUNET_JSON_pack_timestamp ("stamp_expire_legal", - dk->meta.expire_legal), - key_spec - ); - GNUNET_assert (NULL != entry); - } - - /* Build up the running hash of all master signatures of the - denominations */ - append_signature (&sig_ctx, - group->group_off, - (unsigned int) json_array_size (group->list), - &dk->master_sig); - /* Finally, add the denomination to the list of denominations in this - group */ - GNUNET_assert (json_is_array (group->list)); - GNUNET_assert (0 == - json_array_append_new (group->list, - entry)); + &sig_ctx, + dk, + &has_age_restricted_denomination); + if (dk->withdraw_relevant) + { + withdraw_list_issue_date = dk->meta.start; + append_denomination (withdraw_denominations_by_group, + withdraw_denominations, + &withdraw_sig_ctx, + dk, + &withdraw_has_age_restricted_denomination); } } /* loop over heap ends */ @@ -2421,6 +2863,12 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) &free_group, NULL); GNUNET_CONTAINER_multihashmap_destroy (denominations_by_group); + GNUNET_CONTAINER_multihashmap_iterate ( + withdraw_denominations_by_group, + &free_group, + NULL); + GNUNET_CONTAINER_multihashmap_destroy ( + withdraw_denominations_by_group); } GNUNET_CONTAINER_heap_destroy (heap); @@ -2436,13 +2884,43 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh) last_cherry_pick_date, sctx.signkeys, recoup, - grouped_denominations)) + grouped_denominations, + ksh->auditors, + &ksh->krd_array, + &ksh->krd_array_length, + false)) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Failed to generate key response data for %s\n", GNUNET_TIME_timestamp2s (last_cherry_pick_date)); goto CLEANUP; } + if (0 != withdraw_denomination_count) + { + struct GNUNET_HashCode withdraw_hc; + + GNUNET_assert (! GNUNET_TIME_absolute_is_zero ( + withdraw_list_issue_date.abs_time)); + compute_msig_hash (&withdraw_sig_ctx, + &withdraw_hc); + if (GNUNET_OK != + create_krd (ksh, + &withdraw_hc, + withdraw_list_issue_date, + sctx.signkeys, + recoup, + withdraw_denominations, + withdraw_auditors, + &ksh->withdraw_krd_array, + &ksh->withdraw_krd_array_length, + true)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to generate withdrawal-focused key response data\n"); + goto CLEANUP; + } + GNUNET_assert (1 == ksh->withdraw_krd_array_length); + } ksh->management_only = false; /* Sanity check: Make sure that age restriction is enabled IFF at least @@ -2472,7 +2950,13 @@ CLEANUP: GNUNET_array_grow (sig_ctx.elements, sig_ctx.elements_size, 0); + GNUNET_array_grow (withdraw_sig_ctx.elements, + withdraw_sig_ctx.elements_size, + 0); json_decref (grouped_denominations); + json_decref (withdraw_denominations); + if (NULL != withdraw_auditors) + json_decref (withdraw_auditors); if (NULL != sctx.signkeys) json_decref (sctx.signkeys); json_decref (recoup); @@ -2571,6 +3055,7 @@ build_key_state (bool management_only) ksh = GNUNET_new (struct TEH_KeyStateHandle); ksh->signature_expires = GNUNET_TIME_UNIT_FOREVER_TS; + ksh->withdraw_expiration = GNUNET_TIME_UNIT_FOREVER_TS; ksh->reload_time = GNUNET_TIME_timestamp_get (); /* We must use the key_generation from when we STARTED the process! */ ksh->key_generation = key_generation; @@ -2918,18 +3403,39 @@ TEH_keys_get_handler (struct TEH_RequestContext *rc, const char *const args[]) { struct GNUNET_TIME_Timestamp last_issue_date; + const char *have_cherrypick; + const char *denom_filter; + bool withdraw_filter = false; const char *etag; etag = MHD_lookup_connection_value (rc->connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_IF_NONE_MATCH); + denom_filter = MHD_lookup_connection_value (rc->connection, + MHD_GET_ARGUMENT_KIND, + "denom_filter"); + if (NULL != denom_filter) + { + if (0 != strcmp (denom_filter, + "withdraw")) + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + denom_filter); + withdraw_filter = true; + } (void) args; { - const char *have_cherrypick; - have_cherrypick = MHD_lookup_connection_value (rc->connection, MHD_GET_ARGUMENT_KIND, "last_issue_date"); + if (withdraw_filter && + (NULL != have_cherrypick)) + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "denom_filter and last_issue_date are mutually exclusive"); if (NULL != have_cherrypick) { unsigned long long cherrypickn; @@ -2982,37 +3488,55 @@ TEH_keys_get_handler (struct TEH_RequestContext *rc, } return suspend_request (rc->connection); } - krd = bsearch (&last_issue_date, - ksh->krd_array, - ksh->krd_array_length, - sizeof (struct KeysResponseData), - &krd_search_comparator); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Filtering /keys by cherry pick date %s found entry %u/%u\n", - GNUNET_TIME_timestamp2s (last_issue_date), - (unsigned int) (krd - ksh->krd_array), - ksh->krd_array_length); - if ( (NULL == krd) && - (ksh->krd_array_length > 0) ) + if (withdraw_filter) { - if (! GNUNET_TIME_absolute_is_zero (last_issue_date.abs_time)) - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Client provided invalid cherry picking timestamp %s, returning full response\n", - GNUNET_TIME_timestamp2s (last_issue_date)); - krd = &ksh->krd_array[ksh->krd_array_length - 1]; + if (0 == ksh->withdraw_krd_array_length) + return TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_SERVICE_UNAVAILABLE, + TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING, + "no current or future withdrawal denomination keys"); + GNUNET_assert (1 == ksh->withdraw_krd_array_length); + krd = &ksh->withdraw_krd_array[0]; } - if (NULL == krd) + else { - /* Likely keys not ready *yet*. - Wait until they are. */ - return suspend_request (rc->connection); + krd = bsearch (&last_issue_date, + ksh->krd_array, + ksh->krd_array_length, + sizeof (struct KeysResponseData), + &krd_search_comparator); + if (NULL != krd) + GNUNET_log ( + GNUNET_ERROR_TYPE_INFO, + "Filtering /keys by cherry pick date %s found entry %u/%u\n", + GNUNET_TIME_timestamp2s (last_issue_date), + (unsigned int) (krd - ksh->krd_array), + ksh->krd_array_length); + if ( (NULL == krd) && + (ksh->krd_array_length > 0) ) + { + if (! GNUNET_TIME_absolute_is_zero (last_issue_date.abs_time)) + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Client provided invalid cherry picking timestamp %s, returning full response\n", + GNUNET_TIME_timestamp2s (last_issue_date)); + krd = &ksh->krd_array[ksh->krd_array_length - 1]; + } + if (NULL == krd) + { + /* Likely keys not ready *yet*. + Wait until they are. */ + return suspend_request (rc->connection); + } } if ( (NULL != etag) && (0 == strcmp (etag, krd->etag)) ) return TEH_RESPONSE_reply_not_modified (rc->connection, krd->etag, - &setup_general_response_headers, + withdraw_filter + ? &setup_withdraw_response_headers + : &setup_general_response_headers, ksh); return MHD_queue_response ( diff --git a/src/exchange/taler-exchange-httpd_get-keys.h b/src/exchange/taler-exchange-httpd_get-keys.h @@ -79,6 +79,12 @@ struct TEH_DenominationKey */ bool recoup_possible; + /** + * Set while building the public key state if this denomination belongs in + * the withdrawal-focused `/keys` response. + */ + bool withdraw_relevant; + };