/*
This file is part of TALER
Copyright (C) 2023 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with
TALER; see the file COPYING. If not, see
*/
/**
* @file taler-exchange-httpd_age-withdraw_reveal.c
* @brief Handle /age-withdraw/$ACH/reveal requests
* @author Özgür Kesim
*/
#include "platform.h"
#include
#include
#include
#include
#include "taler-exchange-httpd_metrics.h"
#include "taler_error_codes.h"
#include "taler_exchangedb_plugin.h"
#include "taler_mhd_lib.h"
#include "taler-exchange-httpd_mhd.h"
#include "taler-exchange-httpd_age-withdraw_reveal.h"
#include "taler-exchange-httpd_responses.h"
#include "taler-exchange-httpd_keys.h"
/**
* State for an /age-withdraw/$ACH/reveal operation.
*/
struct AgeRevealContext
{
/**
* Commitment for the age-withdraw operation, previously called by the
* client.
*/
struct TALER_AgeWithdrawCommitmentHashP ach;
/**
* Public key of the reserve for with the age-withdraw commitment was
* originally made. This parameter is provided by the client again
* during the call to reveal in order to save a database-lookup.
*/
struct TALER_ReservePublicKeyP reserve_pub;
/**
* Number of coins to reveal. MUST be equal to
* @e num_secrets/(kappa -1).
*/
uint32_t num_coins;
/**
* Number of secrets in the reveal. MUST be a multiple of (kappa-1).
*/
uint32_t num_secrets;
/**
* @e num_secrets secrets for disclosed coins.
*/
struct TALER_PlanchetMasterSecretP *disclosed_coin_secrets;
/**
* The data from the original age-withdraw. Will be retrieved from
* the DB via @a ach and @a reserve_pub.
*/
struct TALER_EXCHANGEDB_AgeWithdraw commitment;
};
/**
* Parse the json body of an '/age-withdraw/$ACH/reveal' request. It extracts
* the denomination hashes, blinded coins and disclosed coins and allocates
* memory for those.
*
* @param connection The MHD connection to handle
* @param j_disclosed_coin_secrets The n*(kappa-1) disclosed coins' private keys in JSON format, from which all other attributes (age restriction, blinding, nonce) will be derived from
* @param[out] actx The context of the operation, only partially built at call time
* @param[out] mhd_ret The result if a reply is queued for MHD
* @return true on success, false on failure, with a reply already queued for MHD.
*/
static enum GNUNET_GenericReturnValue
parse_age_withdraw_reveal_json (
struct MHD_Connection *connection,
const json_t *j_disclosed_coin_secrets,
struct AgeRevealContext *actx,
MHD_RESULT *mhd_ret)
{
enum GNUNET_GenericReturnValue result = GNUNET_SYSERR;
size_t num_entries;
/* Verify JSON-structure consistency */
{
const char *error = NULL;
num_entries = json_array_size (j_disclosed_coin_secrets); /* 0, if not an array */
if (! json_is_array (j_disclosed_coin_secrets))
error = "disclosed_coin_secrets must be an array";
else if (num_entries == 0)
error = "disclosed_coin_secrets must not be empty";
else if (num_entries > TALER_MAX_FRESH_COINS)
error = "maximum number of coins that can be withdrawn has been exceeded";
if (NULL != error)
{
*mhd_ret = TALER_MHD_reply_with_ec (connection,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
error);
return GNUNET_SYSERR;
}
actx->num_secrets = num_entries * (TALER_CNC_KAPPA - 1);
actx->num_coins = num_entries;
}
/* Continue parsing the parts */
{
unsigned int idx = 0;
unsigned int k = 0;
json_t *array = NULL;
json_t *value = NULL;
/* Parse diclosed keys */
actx->disclosed_coin_secrets =
GNUNET_new_array (actx->num_secrets,
struct TALER_PlanchetMasterSecretP);
json_array_foreach (j_disclosed_coin_secrets, idx, array) {
if (! json_is_array (array) ||
(TALER_CNC_KAPPA - 1 != json_array_size (array)))
{
char msg[256] = {0};
GNUNET_snprintf (msg,
sizeof(msg),
"couldn't parse entry no. %d in array disclosed_coin_secrets",
idx + 1);
*mhd_ret = TALER_MHD_reply_with_ec (connection,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
msg);
goto EXIT;
}
json_array_foreach (array, k, value)
{
struct TALER_PlanchetMasterSecretP *secret =
&actx->disclosed_coin_secrets[2 * idx + k];
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_fixed_auto (NULL, secret),
GNUNET_JSON_spec_end ()
};
if (GNUNET_OK !=
GNUNET_JSON_parse (value, spec, NULL, NULL))
{
char msg[256] = {0};
GNUNET_snprintf (msg,
sizeof(msg),
"couldn't parse entry no. %d in array disclosed_coin_secrets[%d]",
k + 1,
idx + 1);
*mhd_ret = TALER_MHD_reply_with_ec (connection,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
msg);
goto EXIT;
}
}
};
}
result = GNUNET_OK;
EXIT:
return result;
}
/**
* Check if the request belongs to an existing age-withdraw request.
* If so, sets the commitment object with the request data.
* Otherwise, it queues an appropriate MHD response.
*
* @param connection The HTTP connection to the client
* @param h_commitment Original commitment value sent with the age-withdraw request
* @param reserve_pub Reserve public key used in the original age-withdraw request
* @param[out] commitment Data from the original age-withdraw request
* @param[out] result In the error cases, a response will be queued with MHD and this will be the result.
* @return #GNUNET_OK if the withdraw request has been found,
* #GNUNET_SYSERR if we did not find the request in the DB
*/
static enum GNUNET_GenericReturnValue
find_original_commitment (
struct MHD_Connection *connection,
const struct TALER_AgeWithdrawCommitmentHashP *h_commitment,
const struct TALER_ReservePublicKeyP *reserve_pub,
struct TALER_EXCHANGEDB_AgeWithdraw *commitment,
MHD_RESULT *result)
{
enum GNUNET_DB_QueryStatus qs;
for (unsigned int try = 0; try < 3; try++)
{
qs = TEH_plugin->get_age_withdraw (TEH_plugin->cls,
reserve_pub,
h_commitment,
commitment);
switch (qs)
{
case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
return GNUNET_OK; /* Only happy case */
case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
*result = TALER_MHD_reply_with_error (connection,
MHD_HTTP_NOT_FOUND,
TALER_EC_EXCHANGE_AGE_WITHDRAW_COMMITMENT_UNKNOWN,
NULL);
return GNUNET_SYSERR;
case GNUNET_DB_STATUS_HARD_ERROR:
*result = TALER_MHD_reply_with_ec (connection,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"get_age_withdraw_info");
return GNUNET_SYSERR;
case GNUNET_DB_STATUS_SOFT_ERROR:
break; /* try again */
default:
GNUNET_break (0);
*result = TALER_MHD_reply_with_ec (connection,
TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
NULL);
return GNUNET_SYSERR;
}
}
/* after unsuccessful retries*/
*result = TALER_MHD_reply_with_ec (connection,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"get_age_withdraw_info");
return GNUNET_SYSERR;
}
/**
* @brief Derives a age-restricted planchet from a given secret and calculates the hash
*
* @param connection Connection to the client
* @param keys The denomination keys in memory
* @param secret The secret to a planchet
* @param denom_pub_h The hash of the denomination for the planchet
* @param max_age The maximum age allowed
* @param[out] bch Hashcode to write
* @param[out] result On error, a HTTP-response will be queued and result set accordingly
* @return GNUNET_OK on success, GNUNET_SYSERR otherwise, with an error message
* written to the client and @e result set.
*/
static enum GNUNET_GenericReturnValue
calculate_blinded_hash (
struct MHD_Connection *connection,
const struct TEH_KeyStateHandle *keys,
const struct TALER_PlanchetMasterSecretP *secret,
const struct TALER_DenominationHashP *denom_pub_h,
uint8_t max_age,
struct TALER_BlindedCoinHashP *bch,
MHD_RESULT *result)
{
enum GNUNET_GenericReturnValue ret;
struct TEH_DenominationKey *denom_key;
struct TALER_AgeCommitmentHash ach;
/* First, retrieve denomination details */
denom_key = TEH_keys_denomination_by_hash_from_state (keys,
denom_pub_h,
connection,
result);
if (NULL == denom_key)
{
GNUNET_break_op (0);
*result = TALER_MHD_reply_with_ec (connection,
TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
NULL);
return GNUNET_SYSERR;
}
/* calculate age commitment hash */
{
struct TALER_AgeCommitmentProof acp;
TALER_age_restriction_from_secret (secret,
&denom_key->denom_pub.age_mask,
max_age,
&acp);
TALER_age_commitment_hash (&acp.commitment,
&ach);
TALER_age_commitment_proof_free (&acp);
}
/* Next: calculate planchet */
{
struct TALER_CoinPubHashP c_hash;
struct TALER_PlanchetDetail detail = {0};
struct TALER_CoinSpendPrivateKeyP coin_priv;
union GNUNET_CRYPTO_BlindingSecretP bks;
struct GNUNET_CRYPTO_BlindingInputValues bi = {
.cipher = denom_key->denom_pub.bsign_pub_key->cipher
};
struct TALER_ExchangeWithdrawValues alg_values = {
.blinding_inputs = &bi
};
union GNUNET_CRYPTO_BlindSessionNonce nonce;
union GNUNET_CRYPTO_BlindSessionNonce *noncep = NULL;
// FIXME: add logic to denom.c to do this!
if (GNUNET_CRYPTO_BSA_CS == bi.cipher)
{
struct TEH_CsDeriveData cdd = {
.h_denom_pub = &denom_key->h_denom_pub,
.nonce = &nonce.cs_nonce,
};
TALER_cs_withdraw_nonce_derive (secret,
&nonce.cs_nonce);
noncep = &nonce;
GNUNET_assert (TALER_EC_NONE ==
TEH_keys_denomination_cs_r_pub (
&cdd,
false,
&bi.details.cs_values));
}
TALER_planchet_blinding_secret_create (secret,
&alg_values,
&bks);
TALER_planchet_setup_coin_priv (secret,
&alg_values,
&coin_priv);
ret = TALER_planchet_prepare (&denom_key->denom_pub,
&alg_values,
&bks,
noncep,
&coin_priv,
&ach,
&c_hash,
&detail);
if (GNUNET_OK != ret)
{
GNUNET_break (0);
*result = TALER_MHD_reply_json_pack (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
"{ss}",
"details",
"failed to prepare planchet from base key");
return ret;
}
TALER_coin_ev_hash (&detail.blinded_planchet,
&denom_key->h_denom_pub,
bch);
TALER_blinded_planchet_free (&detail.blinded_planchet);
}
return ret;
}
/**
* @brief Checks the validity of the disclosed coins as follows:
* - Derives and calculates the disclosed coins'
* - public keys,
* - nonces (if applicable),
* - age commitments,
* - blindings
* - blinded hashes
* - Computes h_commitment with those calculated and the undisclosed hashes
* - Compares h_commitment with the value from the original commitment
* - Verifies that all public keys in indices larger than the age group
* corresponding to max_age are derived from the constant public key.
*
* The derivation of the blindings, (potential) nonces and age-commitment from
* a coin's private keys is defined in
* https://docs.taler.net/design-documents/024-age-restriction.html#withdraw
*
* @param connection HTTP-connection to the client
* @param commitment Original commitment
* @param disclosed_coin_secrets The secrets of the disclosed coins, (TALER_CNC_KAPPA - 1)*num_coins many
* @param num_coins number of coins to reveal via @a disclosed_coin_secrets
* @param[out] result On error, a HTTP-response will be queued and result set accordingly
* @return GNUNET_OK on success, GNUNET_SYSERR otherwise
*/
static enum GNUNET_GenericReturnValue
verify_commitment_and_max_age (
struct MHD_Connection *connection,
const struct TALER_EXCHANGEDB_AgeWithdraw *commitment,
const struct TALER_PlanchetMasterSecretP *disclosed_coin_secrets,
uint32_t num_coins,
MHD_RESULT *result)
{
enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
struct GNUNET_HashContext *hash_context;
struct TEH_KeyStateHandle *keys;
if (num_coins != commitment->num_coins)
{
GNUNET_break_op (0);
*result = TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"#coins");
return GNUNET_SYSERR;
}
/* We need the current keys in memory for the meta-data of the denominations */
keys = TEH_keys_get_state ();
if (NULL == keys)
{
*result = TALER_MHD_reply_with_ec (connection,
TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
NULL);
return GNUNET_SYSERR;
}
hash_context = GNUNET_CRYPTO_hash_context_start ();
for (size_t coin_idx = 0; coin_idx < num_coins; coin_idx++)
{
size_t i = 0; /* either 0 or 1, to index into coin_evs */
for (size_t k = 0; knoreveal_index)
{
GNUNET_CRYPTO_hash_context_read (hash_context,
&commitment->h_coin_evs[coin_idx],
sizeof(commitment->h_coin_evs[coin_idx]));
}
else
{
/* j is the index into disclosed_coin_secrets[] */
size_t j = (TALER_CNC_KAPPA - 1) * coin_idx + i;
const struct TALER_PlanchetMasterSecretP *secret;
struct TALER_BlindedCoinHashP bch;
GNUNET_assert (2>i);
GNUNET_assert ((TALER_CNC_KAPPA - 1) * num_coins > j);
secret = &disclosed_coin_secrets[j];
i++;
ret = calculate_blinded_hash (connection,
keys,
secret,
&commitment->denom_pub_hashes[coin_idx],
commitment->max_age,
&bch,
result);
if (GNUNET_OK != ret)
{
GNUNET_CRYPTO_hash_context_abort (hash_context);
return GNUNET_SYSERR;
}
/* Continue the running hash of all coin hashes with the calculated
* hash-value of the current, disclosed coin */
GNUNET_CRYPTO_hash_context_read (hash_context,
&bch,
sizeof(bch));
}
}
}
/* Finally, compare the calculated hash with the original commitment */
{
struct GNUNET_HashCode calc_hash;
GNUNET_CRYPTO_hash_context_finish (hash_context,
&calc_hash);
if (0 != GNUNET_CRYPTO_hash_cmp (&commitment->h_commitment.hash,
&calc_hash))
{
GNUNET_break_op (0);
*result = TALER_MHD_reply_with_ec (connection,
TALER_EC_EXCHANGE_AGE_WITHDRAW_REVEAL_INVALID_HASH,
NULL);
return GNUNET_SYSERR;
}
}
return GNUNET_OK;
}
/**
* @brief Send a response for "/age-withdraw/$RCH/reveal"
*
* @param connection The http connection to the client to send the response to
* @param commitment The data from the commitment with signatures
* @return a MHD result code
*/
static MHD_RESULT
reply_age_withdraw_reveal_success (
struct MHD_Connection *connection,
const struct TALER_EXCHANGEDB_AgeWithdraw *commitment)
{
json_t *list = json_array ();
GNUNET_assert (NULL != list);
for (unsigned int i = 0; i < commitment->num_coins; i++)
{
json_t *obj = GNUNET_JSON_PACK (
TALER_JSON_pack_blinded_denom_sig (NULL,
&commitment->denom_sigs[i]));
GNUNET_assert (0 ==
json_array_append_new (list,
obj));
}
return TALER_MHD_REPLY_JSON_PACK (
connection,
MHD_HTTP_OK,
GNUNET_JSON_pack_array_steal ("ev_sigs",
list));
}
MHD_RESULT
TEH_handler_age_withdraw_reveal (
struct TEH_RequestContext *rc,
const struct TALER_AgeWithdrawCommitmentHashP *ach,
const json_t *root)
{
MHD_RESULT result = MHD_NO;
enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
struct AgeRevealContext actx = {0};
const json_t *j_disclosed_coin_secrets;
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_fixed_auto ("reserve_pub",
&actx.reserve_pub),
GNUNET_JSON_spec_array_const ("disclosed_coin_secrets",
&j_disclosed_coin_secrets),
GNUNET_JSON_spec_end ()
};
actx.ach = *ach;
/* Parse JSON body*/
ret = TALER_MHD_parse_json_data (rc->connection,
root,
spec);
if (GNUNET_OK != ret)
{
GNUNET_break_op (0);
return (GNUNET_SYSERR == ret) ? MHD_NO : MHD_YES;
}
do {
/* Extract denominations, blinded and disclosed coins */
if (GNUNET_OK !=
parse_age_withdraw_reveal_json (
rc->connection,
j_disclosed_coin_secrets,
&actx,
&result))
break;
/* Find original commitment */
if (GNUNET_OK !=
find_original_commitment (
rc->connection,
&actx.ach,
&actx.reserve_pub,
&actx.commitment,
&result))
break;
/* Verify the computed h_commitment equals the committed one and that coins
* have a maximum age group corresponding max_age (age-mask dependent) */
if (GNUNET_OK !=
verify_commitment_and_max_age (
rc->connection,
&actx.commitment,
actx.disclosed_coin_secrets,
actx.num_coins,
&result))
break;
/* Finally, return the signatures */
result = reply_age_withdraw_reveal_success (rc->connection,
&actx.commitment);
} while (0);
GNUNET_JSON_parse_free (spec);
if (NULL != actx.commitment.denom_sigs)
for (unsigned int i = 0; i