diff options
Diffstat (limited to 'src/backend')
9 files changed, 1957 insertions, 317 deletions
diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c index e9ee1cbf..a7c1cbb4 100644 --- a/src/backend/taler-merchant-httpd.c +++ b/src/backend/taler-merchant-httpd.c @@ -2264,6 +2264,10 @@ run (void *cls, return; } + /* TODO: Load config variables for merchant token family + cipher type "rsa" or "cs" and key size. + Defaults to "rsa" and 2048 bits. */ + if (GNUNET_OK != TALER_CONFIG_parse_currencies (cfg, &TMH_num_cspecs, diff --git a/src/backend/taler-merchant-httpd_contract.c b/src/backend/taler-merchant-httpd_contract.c index 38c82e70..e7c7ac49 100644 --- a/src/backend/taler-merchant-httpd_contract.c +++ b/src/backend/taler-merchant-httpd_contract.c @@ -19,11 +19,13 @@ * @author Christian Blättler */ #include "platform.h" +#include <gnunet/gnunet_common.h> #include <jansson.h> +#include <stdint.h> #include "taler-merchant-httpd_contract.h" enum TALER_MerchantContractInputType -TMH_string_to_contract_input_type (const char *str) +TMH_contract_input_type_from_string (const char *str) { /* For now, only 'token' is the only supported option. */ if (0 == strcmp("token", str)) @@ -35,7 +37,7 @@ TMH_string_to_contract_input_type (const char *str) } enum TALER_MerchantContractOutputType -TMH_string_to_contract_output_type (const char *str) +TMH_contract_output_type_from_string (const char *str) { /* For now, only 'token' is the only supported option. */ if (0 == strcmp("token", str)) @@ -45,3 +47,412 @@ TMH_string_to_contract_output_type (const char *str) return TALER_MCOT_INVALID; } + +const char * +TMH_string_from_contract_input_type (enum TALER_MerchantContractInputType t) +{ + switch (t) { + case TALER_MCIT_TOKEN: + return "token"; + case TALER_MCIT_COIN: + return "coin"; + default: + return "invalid"; + } +} + +const char * +TMH_string_from_contract_output_type (enum TALER_MerchantContractOutputType t) +{ + switch (t) { + case TALER_MCOT_TOKEN: + return "token"; + case TALER_MCOT_COIN: + return "coin"; + case TALER_MCOT_TAX_RECEIPT: + return "tax_receipt"; + default: + return "invalid"; + } +} + +/** + * Parse given JSON object to choices array. + * + * @param cls closure, pointer to array length + * @param root the json array representing the choices + * @param[out] spec where to write the data + * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error + */ +static enum GNUNET_GenericReturnValue +parse_choices (void *cls, + json_t *root, + struct GNUNET_JSON_Specification *spec) +{ + struct TALER_MerchantContractChoice **choices = spec->ptr; + unsigned int *choices_len = cls; + if (!json_is_array (root)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + GNUNET_array_grow (*choices, + *choices_len, + json_array_size (root)); + + for (unsigned int i = 0; i<*choices_len; i++) + { + const json_t *jinputs; + const json_t *joutputs; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("inputs", + &jinputs), + GNUNET_JSON_spec_array_const ("outputs", + &joutputs), + GNUNET_JSON_spec_end () + }; + const char *error_name; + unsigned int error_line; + struct TALER_MerchantContractChoice *choice = &(*choices)[i]; + + if (GNUNET_OK != + GNUNET_JSON_parse (json_array_get (root, i), + spec, + &error_name, + &error_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse %s at %u: %s\n", + spec[error_line].field, + error_line, + error_name); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + { + const json_t *jinput; + size_t idx; + json_array_foreach ((json_t *) jinputs, idx, jinput) + { + struct TALER_MerchantContractInput input = {.details.token.count = 1}; + const char *kind; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_string ("kind", + &kind), + GNUNET_JSON_spec_string ("token_family_slug", + &input.details.token.token_family_slug), + GNUNET_JSON_spec_timestamp ("valid_after", + &input.details.token.valid_after), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("count", + &input.details.token.count), + NULL), + GNUNET_JSON_spec_end() + }; + const char *ierror_name; + unsigned int ierror_line; + + if (GNUNET_OK != + GNUNET_JSON_parse (jinput, + ispec, + &ierror_name, + &ierror_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse %s at %u: %s\n", + spec[ierror_line].field, + ierror_line, + ierror_name); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + input.type = TMH_contract_input_type_from_string (kind); + + if (TALER_MCIT_INVALID == input.type) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Field 'kind' invalid in input #%u\n", + (unsigned int) idx); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + if (0 == input.details.token.count) + { + /* Ignore inputs with 'number' field set to 0 */ + continue; + } + + GNUNET_array_append (choice->inputs, + choice->inputs_len, + input); + } + } + + { + const json_t *joutput; + size_t idx; + json_array_foreach ((json_t *) joutputs, idx, joutput) + { + struct TALER_MerchantContractOutput output = {.details.token.count = 1}; + const char *kind; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_string ("kind", + &kind), + GNUNET_JSON_spec_string ("token_family_slug", + &output.details.token.token_family_slug), + GNUNET_JSON_spec_timestamp ("valid_after", + &output.details.token.valid_after), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("count", + &output.details.token.count), + NULL), + GNUNET_JSON_spec_end() + }; + const char *ierror_name; + unsigned int ierror_line; + + if (GNUNET_OK != + GNUNET_JSON_parse (joutput, + ispec, + &ierror_name, + &ierror_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse %s at %u: %s\n", + spec[ierror_line].field, + ierror_line, + ierror_name); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + output.type = TMH_contract_output_type_from_string (kind); + + if (TALER_MCOT_INVALID == output.type) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Field 'kind' invalid in output #%u\n", + (unsigned int) idx); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + if (0 == output.details.token.count) + { + /* Ignore outputs with 'number' field set to 0 */ + continue; + } + + GNUNET_array_append (choice->outputs, + choice->outputs_len, + output); + } + } + } + + return GNUNET_OK; +} + + +struct GNUNET_JSON_Specification +TALER_JSON_spec_choices (const char *name, + struct TALER_MerchantContractChoice **choices, + unsigned int *choices_len) +{ + struct GNUNET_JSON_Specification ret = { + .cls = (void *) choices_len, + .parser = &parse_choices, + .field = name, + .ptr = choices, + }; + + return ret; +} + + +/** + * Parse given JSON object to token families array. + * + * @param cls closure, pointer to array length + * @param root the json object representing the token families. The keys are + * the token family slugs. + * @param[out] spec where to write the data + * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error + */ +static enum GNUNET_GenericReturnValue +parse_token_families (void *cls, + json_t *root, + struct GNUNET_JSON_Specification *spec) +{ + struct TALER_MerchantContractTokenFamily **families = spec->ptr; + unsigned int *families_len = cls; + json_t *jfamily; + const char *slug; + if (!json_is_object (root)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + json_object_foreach(root, slug, jfamily) + { + const json_t *keys; + struct TALER_MerchantContractTokenFamily family = { + .slug = slug + }; + + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("keys", + &keys), + 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), + // GNUNET_JSON_spec_object_const ("description_i18n", + // &family.description_i18n), + }; + const char *error_name; + unsigned int error_line; + + if (GNUNET_OK != + GNUNET_JSON_parse (jfamily, + spec, + &error_name, + &error_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse %s at %u: %s\n", + spec[error_line].field, + error_line, + error_name); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + GNUNET_array_grow (family.keys, + family.keys_len, + json_array_size (keys)); + + for (unsigned int i = 0; i<family.keys_len; i++) + { + /* TODO: Move this to TALER_JSON_spec_token_issue_key */ + int64_t cipher; + struct TALER_MerchantContractTokenFamilyKey *key; + key = &family.keys[i]; + /* TODO: Free when not used anymore */ + key->pub.public_key = GNUNET_new (struct GNUNET_CRYPTO_BlindSignPublicKey); + struct GNUNET_JSON_Specification key_spec[] = { + GNUNET_JSON_spec_fixed_auto ("h_pub", + &key->pub.public_key->pub_key_hash), + GNUNET_JSON_spec_rsa_public_key ("rsa_pub", + &key->pub.public_key->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", + &key->valid_after), + GNUNET_JSON_spec_timestamp ("valid_before", + &key->valid_before), + GNUNET_JSON_spec_end() + }; + const char *ierror_name; + unsigned int ierror_line; + + if (GNUNET_OK != + GNUNET_JSON_parse (json_array_get (keys, i), + key_spec, + &ierror_name, + &ierror_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse %s at %u: %s\n", + key_spec[ierror_line].field, + ierror_line, + ierror_name); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + switch (cipher) { + case GNUNET_CRYPTO_BSA_RSA: + key->pub.public_key->cipher = GNUNET_CRYPTO_BSA_RSA; + break; + case GNUNET_CRYPTO_BSA_CS: + key->pub.public_key->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); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + + GNUNET_array_append (*families, *families_len, family); + } + + return GNUNET_OK; +} + + +struct GNUNET_JSON_Specification +TALER_JSON_spec_token_families (const char *name, + struct TALER_MerchantContractTokenFamily **families, + unsigned int *families_len) +{ + struct GNUNET_JSON_Specification ret = { + .cls = (void *) families_len, + .parser = &parse_token_families, + .field = name, + .ptr = families, + }; + + 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 b1e3938c..6389cc70 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> @@ -251,102 +252,92 @@ struct TALER_MerchantContractChoice unsigned int outputs_len; }; -struct TALER_MerchantContractLimits +/** + * Public key and corresponding metadata for a token family. + */ +struct TALER_MerchantContractTokenFamilyKey { /** - * Currency these limits are for. + * Public key. */ - char currency[TALER_CURRENCY_LEN]; + struct TALER_TokenIssuePublicKeyP pub; /** - * The hash of the merchant instance's wire details. + * Tokens signed by this key will be valid after this time. */ - struct TALER_MerchantWireHashP h_wire; + struct GNUNET_TIME_Timestamp valid_after; /** - * Wire transfer method identifier for the wire method associated with ``h_wire``. - * The wallet may only select exchanges via a matching auditor if the - * exchange also supports this wire method. - * The wire transfer fees must be added based on this wire transfer method. + * Tokens signed by this key will be valid before this time. */ - char *wire_method; + struct GNUNET_TIME_Timestamp valid_before; +}; +struct TALER_MerchantContractTokenFamily +{ /** - * Maximum total deposit fee accepted by the merchant for this contract. + * Slug of the token family. */ - struct TALER_Amount max_fee; -}; + const char *slug; -struct TALER_MerchantContractTokenAuthority -{ /** - * Label of the token authority. - */ - const char *label; + * Human-readable name of the token family. + */ + char *name; /** - * Human-readable description of the semantics of the tokens issued by - * this authority. - */ + * Human-readable description of the semantics of the tokens issued by + * this token family. + */ char *description; /** - * Map from IETF BCP 47 language tags to localized description. - */ + * Map from IETF BCP 47 language tags to localized description. + */ json_t *description_i18n; /** - * Public key used to validate tokens signed by this authority. - */ - struct TALER_TokenFamilyPublicKey *pub; + * Relevant public keys of this token family for the given contract. + */ + struct TALER_MerchantContractTokenFamilyKey *keys; /** - * When will tokens signed by this key expire? - */ - struct GNUNET_TIME_Timestamp token_expiration; + * Length of the @e keys array. + */ + unsigned int keys_len; /** - * Must a wallet understand this token type to process contracts that - * consume or yield it? - */ + * Must a wallet understand this token type to process contracts that + * consume or yield it? + */ bool critical; /** - * Kind of the token. - */ + * Kind of the token family. + */ enum TALER_MerchantContractTokenKind kind; /** - * Kind-specific information about the token. - */ + * Kind-specific information about the token. + */ union { /** - * Subscription token. - */ + * Subscription token. + */ 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 - * this type if the respective contract says so). May contain "*" for - * any domain or subdomain. - */ + * Array of domain names where this subscription can be safely used + * (e.g. the issuer warrants that these sites will re-issue tokens of + * this type if the respective contract says so). May contain "*" for + * any domain or subdomain. + */ const char **trusted_domains; /** - * Length of the @e trusted_domains array. - */ + * Length of the @e trusted_domains array. + */ unsigned int trusted_domains_len; } subscription; @@ -356,19 +347,19 @@ struct TALER_MerchantContractTokenAuthority struct { /** - * Array of domain names where this discount token is intended to be - * used. May contain "*" for any domain or subdomain. Users should be - * warned about sites proposing to consume discount tokens of this - * type that are not in this list that the merchant is accepting a - * coupon from a competitor and thus may be attaching different - * semantics (like get 20% discount for my competitors 30% discount - * token). - */ + * Array of domain names where this discount token is intended to be + * used. May contain "*" for any domain or subdomain. Users should be + * warned about sites proposing to consume discount tokens of this + * type that are not in this list that the merchant is accepting a + * coupon from a competitor and thus may be attaching different + * semantics (like get 20% discount for my competitors 30% discount + * token). + */ const char **expected_domains; /** - * Length of the @e expected_domains array. - */ + * Length of the @e expected_domains array. + */ unsigned int expected_domains_len; } discount; @@ -540,7 +531,7 @@ struct TALER_MerchantContract /** * Array of token authorities. */ - struct TALER_MerchantContractTokenAuthority *token_authorities; + struct TALER_MerchantContractTokenFamily *token_authorities; /** * Length of the @e token_authorities array. @@ -556,10 +547,16 @@ struct TALER_MerchantContract }; enum TALER_MerchantContractInputType -TMH_string_to_contract_input_type (const char *str); +TMH_contract_input_type_from_string (const char *str); enum TALER_MerchantContractOutputType -TMH_string_to_contract_output_type (const char *str); +TMH_contract_output_type_from_string (const char *str); + +const char * +TMH_string_from_contract_input_type (enum TALER_MerchantContractInputType t); + +const char * +TMH_string_from_contract_output_type (enum TALER_MerchantContractOutputType t); /** * Serialize @a contract to a JSON object, ready to be stored in the database. @@ -589,4 +586,53 @@ enum GNUNET_GenericReturnValue TMH_serialize_contract_v1 (const struct TALER_MerchantContract *contract, const struct TMH_MerchantInstance *instance, json_t *exchanges, - json_t **out);
\ No newline at end of file + json_t **out); + +/** + * Provide specification to parse given JSON array to an array + * of contract choices. + * + * @param name name of the choices field in the JSON + * @param[out] choices pointer to the first element of the array + * @param[out] choices_len pointer to the length of the array + * @return spec for parsing a choices array + */ +struct GNUNET_JSON_Specification +TALER_JSON_spec_choices (const char *name, + struct TALER_MerchantContractChoice **choices, + unsigned int *choices_len); + +/** + * Provide specification to parse given JSON object to an array + * of token families. + * + * @param name name of the token families field in the JSON + * @param[out] families pointer to the first element of the array + * @param[out] families_len pointer to the length of the array + * @return spec for parsing a token families object + */ +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 14edfd55..888ea0ba 100644 --- a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c +++ b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c @@ -25,14 +25,27 @@ * @author Florian Dold */ #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> +#include <microhttpd.h> +#include <stddef.h> +#include <stdint.h> +#include <string.h> #include <taler/taler_dbevents.h> +#include <taler/taler_error_codes.h> #include <taler/taler_signatures.h> #include <taler/taler_json_lib.h> #include <taler/taler_exchange_service.h> +#include "taler-merchant-httpd.h" #include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_contract.h" #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" /** @@ -46,6 +59,16 @@ #define MAX_COIN_ALLOWED_COINS 1024 /** + * Maximum number of tokens that we allow as inputs per transaction + */ +#define MAX_TOKEN_ALLOWED_INPUTs 128 + +/** + * Maximum number of tokens that we allow as outputs per transaction + */ +#define MAX_TOKEN_ALLOWED_OUTPUTs 128 + +/** * How often do we ask the exchange again about our * KYC status? Very rarely, as if the user actively * changes it, we should usually notice anyway. @@ -69,11 +92,21 @@ enum PayPhase PP_INIT = 0, /** + * Parse wallet data object from the pay request. + */ + PP_PARSE_WALLET_DATA, + + /** * Check database state for the given order. */ PP_CHECK_CONTRACT, /** + * Validate provided tokens and token evelopes. + */ + PP_VALIDATE_TOKENS, + + /** * Contract has been paid. */ PP_CONTRACT_PAID, @@ -202,6 +235,75 @@ struct DepositConfirmation }; +struct TokenUseConfirmation +{ + + /** + * Signature on the deposit request made using the token use private key. + */ + struct TALER_TokenUseSignatureP sig; + + /** + * Token use public key. This key was blindly signed by the merchant during + * the token issuance process. + */ + struct TALER_TokenUsePublicKeyP pub; + + /** + * Unblinded signature on the token use public key done by the merchant. + */ + struct TALER_TokenIssueSignatureP unblinded_sig; + + /** + * Hash of the token issue public key associated with this token. + */ + struct TALER_TokenIssuePublicKeyHashP h_issue; + + /** + * true if we found this token in the database. + */ + bool found_in_db; + +}; + + +/** + * 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; + +}; + + +/** + * (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. @@ -274,6 +376,32 @@ struct PayContext struct DepositConfirmation *dc; /** + * 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 SignedOutputToken *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; + + /** + * Array with @e token_families_len token families from the contract terms. + */ + struct TALER_MerchantContractTokenFamily *token_families; + + /** * MHD connection to return to */ struct MHD_Connection *connection; @@ -302,11 +430,32 @@ struct PayContext struct MHD_Response *response; /** + * Index of selected choice in the @e contract_terms choices array. + */ + int64_t choice_index; + + /** * Our contract (or NULL if not available). */ json_t *contract_terms; /** + * Wallet data json object from the request. Containing additional + * wallet data such as the selected choice_index. + */ + const json_t *wallet_data; + + /** + * Hash of the canonicalized wallet data json object. + */ + struct GNUNET_HashCode h_wallet_data; + + /** + * Output commitment hash calculated from the 'tokens_evs' field of the request. + */ + struct GNUNET_HashCode h_outputs; + + /** * Placeholder for #TALER_MHD_parse_post_json() to keep its internal state. */ void *json_parse_context; @@ -421,6 +570,34 @@ struct PayContext size_t coins_cnt; /** + * 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; + + /** + * Length of the @e token_families array. + */ + unsigned int token_families_len; + + /** * Number of exchanges involved in the payment. Length * of the @e eg array. */ @@ -1137,6 +1314,7 @@ AGE_FAIL: .merchant_payto_uri = pc->wm->payto_uri, .wire_salt = pc->wm->wire_salt, .h_contract_terms = pc->h_contract_terms, + .wallet_data_hash = pc->h_wallet_data, .wallet_timestamp = pc->timestamp, .merchant_pub = hc->instance->merchant_pub, .refund_deadline = pc->refund_deadline @@ -1325,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 @@ -1334,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) */ @@ -1347,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, @@ -1354,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); @@ -1851,6 +2058,47 @@ phase_execute_pay_transaction (struct PayContext *pc) return; } + for (size_t i = 0; i<pc->tokens_cnt; i++) + { + struct TokenUseConfirmation *tuc = &pc->tokens[i]; + + enum GNUNET_DB_QueryStatus qs; + + /* Insert used token into database, the unique contraint will + case an error if this token was used before. */ + qs = TMH_db->insert_spent_token (TMH_db->cls, + &pc->h_contract_terms, + &tuc->h_issue, + &tuc->pub, + &tuc->sig, + &tuc->unblinded_sig); + + if (0 > qs) + { + TMH_db->rollback (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return; /* do it again */ + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert used token")); + return; + } + else if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* UNIQUE constraint violation, meaning this token was already used. */ + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_INVALID, + NULL)); + return; + } + } + { enum GNUNET_DB_QueryStatus qs; @@ -1968,6 +2216,34 @@ phase_execute_pay_transaction (struct PayContext *pc) } } + /* Store signed output tokens in database. */ + for (size_t i = 0; i<pc->output_tokens_len; i++) + { + struct SignedOutputToken *output = &pc->output_tokens[i]; + + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->insert_issued_token (TMH_db->cls, + &pc->h_contract_terms, + &output->h_issue, + &output->sig); + + if (0 >= qs) + { + TMH_db->rollback (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return; /* do it again */ + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert output token")); + return; + } + } + TMH_notify_order_change (hc->instance, TMH_OSF_CLAIMED | TMH_OSF_PAID, pc->timestamp, @@ -2026,6 +2302,342 @@ 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_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' has invalid issue 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_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_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++; + } + + 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; +} + + +/** + * 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. + * + * @param[in,out] pc context we use to handle the payment + */ +static void +phase_validate_tokens (struct PayContext *pc) +{ + if (NULL == pc->choices || 0 >= pc->choices_len) + { + /* No tokens to validate */ + pc->phase = PP_PAY_TRANSACTION; + return; + } + + if (pc->choice_index < 0) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order `%s' has non-empty choices array but" + "request is missing 'choice_index' field\n", + pc->order_id); + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_MISSING, + NULL)); + return; + } + + if (pc->choice_index >= pc->choices_len) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order `%s' has choices array with %u elements but " + "request has 'choice_index' field with value %ld\n", + pc->order_id, + pc->choices_len, + pc->choice_index); + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_OUT_OF_BOUNDS, + NULL)); + return; + } + + { + struct TALER_MerchantContractChoice selected; + + selected = pc->choices[pc->choice_index]; + + /* 1. Iterate over inputs of selected choice: + 1.1. Get key for each input. + 1.2. Check if token signed by this key are valid at the current time. + 1.3. Iterate over provided tokens and check if required number with matching h_issue are present. + 1.4. Validate ub_sig with the issue public key, validate token_sig using the token_pub key of the request. + 1.5. Sum up validated tokens and check if validated_len == tokens_cnt after loop. */ + for (unsigned int i = 0; i<selected.inputs_len; i++) + { + struct TALER_MerchantContractInput input = selected.inputs[i]; + struct TALER_MerchantContractTokenFamily family; + struct TALER_MerchantContractTokenFamilyKey key; + + if (input.type != TALER_MCIT_TOKEN) + { + /* only validate inputs of type token (for now) */ + continue; + } + + /* TODO: Replace this with ordering convention. */ + 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. */ + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL)); + return; + } + + struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); + + /* Ensure tokens signed by this key are valid at the current time. */ + if (GNUNET_TIME_timestamp_cmp (key.valid_after, >, now) || + GNUNET_TIME_timestamp_cmp (key.valid_before, <=, now)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Token family key validity period from %s to %s " + "is not valid at the current time\n", + GNUNET_TIME_timestamp2s (key.valid_after), + GNUNET_TIME_timestamp2s (key.valid_before)); + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_INVALID, + NULL)); + return; + } + + if (GNUNET_NO == find_valid_input_tokens (pc, + &key, + input.details.token.count)) + { + /* Error is already scheduled from find_valid_input_token. */ + return; + } + } + + GNUNET_array_grow (pc->output_tokens, + pc->output_tokens_len, + selected.outputs_len); + + 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; + struct TALER_MerchantContractTokenFamilyKey key; + + if (output.type != TALER_MCOT_TOKEN) + { + /* only validate outputs of type tokens (for now) */ + continue; + } + + 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. */ + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + 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; + } + + GNUNET_assert (NULL != details.priv.private_key); + + 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; + } + + } + } + + pc->phase = PP_PAY_TRANSACTION; +} + + +/** * Function called with information about a coin that was deposited. * Checks if this coin is in our list of deposits as well. * @@ -2075,6 +2687,38 @@ deposit_paid_check ( } +static void +input_tokens_paid_check ( + void *cls, + uint64_t spent_token_serial, + const struct TALER_PrivateContractHashP *h_contract_terms, + const struct TALER_TokenIssuePublicKeyHashP *h_issue_pub, + const struct TALER_TokenUsePublicKeyP *use_pub, + const struct TALER_TokenUseSignatureP *use_sig, + const struct TALER_TokenIssueSignatureP *issue_sig) +{ + struct PayContext *pc = cls; + + for (size_t i = 0; i<pc->tokens_cnt; i++) + { + struct TokenUseConfirmation *tuc = &pc->tokens[i]; + + if ( (0 == + GNUNET_CRYPTO_hash_cmp (&tuc->h_issue.hash, + &h_issue_pub->hash)) && + (0 == + GNUNET_memcmp (&tuc->pub, use_pub)) && + (0 == + GNUNET_memcmp (&tuc->sig, use_sig)) && + (0 == + GNUNET_memcmp (&tuc->unblinded_sig, issue_sig)) ) + { + tuc->found_in_db = true; + break; + } + } +} + /** * Handle case where contract was already paid. Either decides * the payment is idempotent, or refunds the excess payment. @@ -2084,31 +2728,64 @@ deposit_paid_check ( static void phase_contract_paid (struct PayContext *pc) { - enum GNUNET_DB_QueryStatus qs; - bool unmatched = false; json_t *refunds; + bool unmatched = false; - qs = TMH_db->lookup_deposits_by_order (TMH_db->cls, - pc->order_serial, - &deposit_paid_check, - pc); - 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, - "lookup_deposits_by_order")); - return; + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_deposits_by_order (TMH_db->cls, + pc->order_serial, + &deposit_paid_check, + pc); + /* Since orders with choices can have a price of zero, + 0 is also a valid query state */ + 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, + "lookup_deposits_by_order")); + return; + } } - for (size_t i = 0; i<pc->coins_cnt; i++) + for (size_t i = 0; i<pc->coins_cnt && !unmatched; i++) { struct DepositConfirmation *dci = &pc->dc[i]; if (! dci->matched_in_db) unmatched = true; } + /* Check if provided input tokens match token in the database */ + { + enum GNUNET_DB_QueryStatus qs; + + /* TODO: Use h_contract instead of order_serial here? */ + qs = TMH_db->lookup_spent_tokens_by_order (TMH_db->cls, + pc->order_serial, + &input_tokens_paid_check, + pc); + + 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, + "lookup_spent_tokens_by_order")); + return; + } + } + for (size_t i = 0; i<pc->tokens_cnt && !unmatched; i++) + { + struct TokenUseConfirmation *tuc = &pc->tokens[i]; + + if (! tuc->found_in_db) + unmatched = true; + } if (! unmatched) { /* Everything fine, idempotent request */ @@ -2120,6 +2797,7 @@ phase_contract_paid (struct PayContext *pc) TALER_merchant_pay_sign (&pc->h_contract_terms, &pc->hc->instance->merchant_priv, &sig); + /* TODO: Add token_sigs to response body. */ pay_end (pc, TALER_MHD_REPLY_JSON_PACK ( pc->connection, @@ -2129,6 +2807,8 @@ phase_contract_paid (struct PayContext *pc) return; } /* Conflict, double-payment detected! */ + /* TODO: What should we do with input tokens? + Currently there is no refund for tokens. */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Client attempted to pay extra for already paid order `%s'\n", pc->order_id); @@ -2288,6 +2968,16 @@ phase_check_contract (struct PayContext *pc) &pc->pay_deadline), GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", &pc->wire_transfer_deadline), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_choices ("choices", + &pc->choices, + &pc->choices_len), + NULL), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_token_families ("token_families", + &pc->token_families, + &pc->token_families_len), + NULL), GNUNET_JSON_spec_fixed_auto ("h_wire", &pc->h_wire), GNUNET_JSON_spec_mark_optional ( @@ -2390,7 +3080,72 @@ phase_check_contract (struct PayContext *pc) } pc->wm = wm; } - pc->phase = PP_PAY_TRANSACTION; + pc->phase = PP_VALIDATE_TOKENS; +} + + +/** + * Try to parse the wallet_data object of the pay request into + * the given context. Schedules an error response in the connection + * on failure. + * + * @param[in,out] pc context we use to handle the payment + */ +static void +phase_parse_wallet_data (struct PayContext *pc) +{ + struct GNUNET_HashCode h_outputs_req; + pc->choice_index = -1; + + // TODO: Ensure that wallet_data must be set for contracts with choices. + if (NULL == pc->wallet_data) + { + pc->phase = PP_CHECK_CONTRACT; + return; + } + + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional( + GNUNET_JSON_spec_int64 ("choice_index", + &pc->choice_index), + NULL), + GNUNET_JSON_spec_mark_optional( + GNUNET_JSON_spec_fixed_auto ("h_outputs", + &h_outputs_req), + NULL), + GNUNET_JSON_spec_end () + }; + + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (pc->connection, + pc->wallet_data, + spec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); + pay_end (pc, + (GNUNET_NO == res) + ? MHD_YES + : MHD_NO); + return; + } + + if (0 != GNUNET_CRYPTO_hash_cmp(&h_outputs_req, &pc->h_outputs)) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "'wallet_data.h_outputs' does not match hash of tokens_evs")); + return; + } + + TALER_json_hash (pc->wallet_data, + &pc->h_wallet_data); + + pc->phase = PP_CHECK_CONTRACT; } @@ -2405,6 +3160,8 @@ 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), @@ -2412,6 +3169,18 @@ phase_parse_pay (struct PayContext *pc) GNUNET_JSON_spec_string ("session_id", &session_id), NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("wallet_data", + &pc->wallet_data), + NULL), + GNUNET_JSON_spec_mark_optional ( + 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 () }; @@ -2443,6 +3212,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) { @@ -2455,7 +3225,6 @@ phase_parse_pay (struct PayContext *pc) "'coins' array too long")); return; } - /* note: 1 coin = 1 deposit confirmation expected */ pc->dc = GNUNET_new_array (pc->coins_cnt, struct DepositConfirmation); @@ -2574,7 +3343,145 @@ phase_parse_pay (struct PayContext *pc) } } } - pc->phase = PP_CHECK_CONTRACT; + + 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; + json_t *token; + + json_array_foreach (tokens, tokens_index, token) + { + struct TokenUseConfirmation *tuc = &pc->tokens[tokens_index]; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_fixed_auto ("token_sig", + &tuc->sig), + GNUNET_JSON_spec_fixed_auto ("token_pub", + &tuc->pub), + TALER_JSON_spec_token_issue_sig ("ub_sig", + &tuc->unblinded_sig), + GNUNET_JSON_spec_fixed_auto ("h_issue", + &tuc->h_issue), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (pc->connection, + token, + 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_index; j++) + { + if (0 == + GNUNET_memcmp (&tuc->pub, + &pc->tokens[j].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 in list")); + return; + } + } + } + } + + 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, + "'tokens_evs' array too long")); + return; + } + if (0 < pc->token_envelopes_cnt) + { + /* Calculate output commitment to be verified later. */ + TALER_json_hash (tokens_evs, + &pc->h_outputs); + + } + + pc->token_envelopes = GNUNET_new_array (pc->token_envelopes_cnt, + struct TokenEnvelope); + + { + 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), + GNUNET_JSON_spec_end () + }; + 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; } @@ -2660,12 +3567,18 @@ TMH_post_orders_ID_pay (const struct TMH_RequestHandler *rh, case PP_INIT: phase_parse_pay (pc); break; + case PP_PARSE_WALLET_DATA: + phase_parse_wallet_data (pc); + break; case PP_CHECK_CONTRACT: phase_check_contract (pc); break; case PP_CONTRACT_PAID: phase_contract_paid (pc); break; + case PP_VALIDATE_TOKENS: + phase_validate_tokens (pc); + break; case PP_PAY_TRANSACTION: phase_execute_pay_transaction (pc); break; diff --git a/src/backend/taler-merchant-httpd_private-delete-orders-ID.c b/src/backend/taler-merchant-httpd_private-delete-orders-ID.c index cd8aa10c..f288ee05 100644 --- a/src/backend/taler-merchant-httpd_private-delete-orders-ID.c +++ b/src/backend/taler-merchant-httpd_private-delete-orders-ID.c @@ -20,6 +20,7 @@ */ #include "platform.h" #include "taler-merchant-httpd_private-delete-orders-ID.h" +#include <stdint.h> #include <taler/taler_json_lib.h> @@ -80,6 +81,7 @@ TMH_private_delete_orders_ID (const struct TMH_RequestHandler *rh, bool paid = false; bool wired = false; bool matches = false; + int16_t choice_index; qs = TMH_db->lookup_order (TMH_db->cls, mi->settings.id, @@ -98,7 +100,8 @@ TMH_private_delete_orders_ID (const struct TMH_RequestHandler *rh, &paid, &wired, &matches, - NULL); + NULL, + &choice_index); } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) return TALER_MHD_reply_with_error (connection, diff --git a/src/backend/taler-merchant-httpd_private-get-orders-ID.c b/src/backend/taler-merchant-httpd_private-get-orders-ID.c index 98653997..50626876 100644 --- a/src/backend/taler-merchant-httpd_private-get-orders-ID.c +++ b/src/backend/taler-merchant-httpd_private-get-orders-ID.c @@ -22,6 +22,9 @@ #include "platform.h" #include "taler-merchant-httpd_private-get-orders-ID.h" #include "taler-merchant-httpd_get-orders-ID.h" +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <stdint.h> #include <taler/taler_json_lib.h> #include <taler/taler_dbevents.h> #include "taler-merchant-httpd_mhd.h" @@ -316,6 +319,12 @@ struct GetOrderRequestContext uint64_t order_serial; /** + * Index of selected choice from ``choices`` array in the contract_terms. + * Is -1 for orders without choices. + */ + int16_t choice_index; + + /** * Total refunds granted for this payment. Only initialized * if @e refunded is set to true. */ @@ -610,6 +619,7 @@ phase_fetch_contract (struct GetOrderRequestContext *gorc) gorc->order_only = false; } TMH_db->preflight (TMH_db->cls); + /* TODO: Check if choice_index is actually set to NULL if not in db. */ qs = TMH_db->lookup_contract_terms3 (TMH_db->cls, hc->instance->settings.id, hc->infix, @@ -619,7 +629,8 @@ phase_fetch_contract (struct GetOrderRequestContext *gorc) &gorc->paid, &gorc->wired, &gorc->paid_session_matches, - &gorc->claim_token); + &gorc->claim_token, + &gorc->choice_index); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "lookup_contract_terms (%s) returned %d\n", hc->infix, @@ -1376,6 +1387,7 @@ phase_reply_result (struct GetOrderRequestContext *gorc) struct TMH_HandlerContext *hc = gorc->hc; MHD_RESULT ret; char *order_status_url; + json_t *choice_index = json_null(); { struct TALER_PrivateContractHashP *h_contract = NULL; @@ -1403,6 +1415,12 @@ phase_reply_result (struct GetOrderRequestContext *gorc) TALER_amount_is_zero (&gorc->contract_amount)); gorc->last_payment = gorc->timestamp; } + if (-1 != gorc->choice_index) { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Choice index is %d\n", + gorc->choice_index); + choice_index = json_integer ((json_int_t) gorc->choice_index); + } ret = TALER_MHD_REPLY_JSON_PACK ( gorc->sc.con, MHD_HTTP_OK, @@ -1440,7 +1458,11 @@ phase_reply_result (struct GetOrderRequestContext *gorc) GNUNET_JSON_pack_array_steal ("refund_details", gorc->refund_details), GNUNET_JSON_pack_string ("order_status_url", - order_status_url)); + order_status_url), + { + .field_name = "choice_index", + .object = choice_index, + }); GNUNET_free (order_status_url); gorc->wire_details = NULL; gorc->refund_details = NULL; diff --git a/src/backend/taler-merchant-httpd_private-get-token-families-SLUG.c b/src/backend/taler-merchant-httpd_private-get-token-families-SLUG.c index 06dbbdf9..12e57a99 100644 --- a/src/backend/taler-merchant-httpd_private-get-token-families-SLUG.c +++ b/src/backend/taler-merchant-httpd_private-get-token-families-SLUG.c @@ -20,6 +20,7 @@ */ #include "platform.h" #include "taler-merchant-httpd_private-get-token-families-SLUG.h" +#include <gnunet/gnunet_json_lib.h> #include <taler/taler_json_lib.h> @@ -84,6 +85,7 @@ TMH_private_get_tokenfamilies_SLUG (const struct TMH_RequestHandler *rh, result = TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_OK, + GNUNET_JSON_pack_string ("slug", details.slug), GNUNET_JSON_pack_string ("name", details.name), GNUNET_JSON_pack_string ("description", details.description), GNUNET_JSON_pack_object_steal ("description_i18n", @@ -91,7 +93,10 @@ TMH_private_get_tokenfamilies_SLUG (const struct TMH_RequestHandler *rh, GNUNET_JSON_pack_timestamp ("valid_after", details.valid_after), GNUNET_JSON_pack_timestamp ("valid_before", details.valid_before), GNUNET_JSON_pack_time_rel ("duration", details.duration), - GNUNET_JSON_pack_string ("kind", kind) + GNUNET_JSON_pack_string ("kind", kind), + // TODO: Implement + GNUNET_JSON_pack_int64 ("issued", 0), + GNUNET_JSON_pack_int64 ("redeemed", 0) ); GNUNET_free (details.name); diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c index 8bdae3f1..a2e3b493 100644 --- a/src/backend/taler-merchant-httpd_private-post-orders.c +++ b/src/backend/taler-merchant-httpd_private-post-orders.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> @@ -34,6 +35,7 @@ #include <taler/taler_error_codes.h> #include <taler/taler_signatures.h> #include <taler/taler_json_lib.h> +#include <time.h> #include "taler-merchant-httpd.h" #include "taler-merchant-httpd_private-post-orders.h" #include "taler-merchant-httpd_exchanges.h" @@ -376,14 +378,14 @@ struct OrderContext unsigned int choices_len; /** - * Array of token types referenced in the contract. + * Array of token families referenced in the contract. */ - struct TALER_MerchantContractTokenAuthority *authorities; + struct TALER_MerchantContractTokenFamily *token_families; /** - * Length of the @e authorities array. + * Length of the @e token_families array. */ - unsigned int authorities_len; + unsigned int token_families_len; } parse_choices; /** @@ -679,7 +681,6 @@ clean_order (void *cls) json_decref (oc->merge_inventory.products); oc->merge_inventory.products = NULL; } - // TODO: Check if this is even correct for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) { GNUNET_array_grow (oc->parse_choices.choices[i].inputs, @@ -689,6 +690,25 @@ clean_order (void *cls) oc->parse_choices.choices[i].outputs_len, 0); } + GNUNET_array_grow (oc->parse_choices.choices, + oc->parse_choices.choices_len, + 0); + for (unsigned int i = 0; i<oc->parse_choices.token_families_len; i++) + { + GNUNET_free (oc->parse_choices.token_families[i].name); + GNUNET_free (oc->parse_choices.token_families[i].description); + json_decref (oc->parse_choices.token_families[i].description_i18n); + for (unsigned int j = 0; j<oc->parse_choices.token_families[i].keys_len; j++) + { + GNUNET_CRYPTO_blind_sign_pub_decref(oc->parse_choices.token_families[i].keys[j].pub.public_key); + } + GNUNET_array_grow (oc->parse_choices.token_families[i].keys, + oc->parse_choices.token_families[i].keys_len, + 0); + } + GNUNET_array_grow (oc->parse_choices.token_families, + oc->parse_choices.token_families_len, + 0); GNUNET_array_grow (oc->parse_request.inventory_products, oc->parse_request.inventory_products_length, 0); @@ -1324,36 +1344,168 @@ get_exchange_keys (void *cls, /** - * Fetch details about the token family with the given @a slug - * and add them to the list of token authorities. Check if the - * token family already has a valid key configured and if not, - * create a new one. + * Get rounded time interval. @a start is calculated by rounding + * @a ts down to the nearest multiple of @a precision. @a end is + * the next higher multiple of @a precision. + * + * @param precision rounding precision. + * year, month, day, hour, minute are supported. + * @param ts timestamp to round + * @param[out] start start of the interval + * @param[out] end end of the interval + * @return #GNUNET_OK on success, #GNUNET_SYSERR on error + */ +static enum GNUNET_GenericReturnValue +get_rounded_time_interval (struct GNUNET_TIME_Relative precision, + struct GNUNET_TIME_Timestamp ts, + struct GNUNET_TIME_Timestamp *start, + struct GNUNET_TIME_Timestamp *end) +{ + struct tm* timeinfo; + time_t seconds; + + seconds = GNUNET_TIME_timestamp_to_s (ts); + timeinfo = localtime (&seconds); + + if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_YEARS, ==, precision)) + { + timeinfo->tm_mon = 0; + timeinfo->tm_mday = 1; + timeinfo->tm_hour = 0; + timeinfo->tm_min = 0; + timeinfo->tm_sec = 0; + } + else if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MONTHS, ==, precision)) + { + timeinfo->tm_mday = 1; + timeinfo->tm_hour = 0; + timeinfo->tm_min = 0; + timeinfo->tm_sec = 0; + } + else if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_DAYS, ==, precision)) + { + timeinfo->tm_hour = 0; + timeinfo->tm_min = 0; + timeinfo->tm_sec = 0; + } + else if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_HOURS, ==, precision)) + { + timeinfo->tm_min = 0; + timeinfo->tm_sec = 0; + } + else if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MINUTES, ==, precision)) + { + timeinfo->tm_sec = 0; + } + else + { + return GNUNET_SYSERR; + } + + *start = GNUNET_TIME_timestamp_from_s (mktime (timeinfo)); + + if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_YEARS, ==, precision)) + { + timeinfo->tm_year++; + } + else if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MONTHS, ==, precision)) + { + timeinfo->tm_mon = (timeinfo->tm_mon + 1) % 12; + } + else if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_DAYS, ==, precision)) + { + timeinfo->tm_mday++; + } + else if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_HOURS, ==, precision)) + { + timeinfo->tm_hour++; + } + else if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MINUTES, ==, precision)) + { + timeinfo->tm_min++; + } + else + { + return GNUNET_SYSERR; + } + + *end = GNUNET_TIME_timestamp_from_s (mktime (timeinfo)); + return GNUNET_OK; +} + +/** + * Check if the token family with the given @a slug is already present in + * the list of token families for this order. If not, fetch its details and + * add it to the list. Then check if there is a public key with a matching + * @a valid_after field. If not, generate a new key pair and store it in the + * database. * * @param[in,out] oc order context * @param slug slug of the token family - * @param start_date validity start date of the token + * @param[in,out] valid_after validity start date of the token, + subject to rounding. Set to the rounded validity + start date of the matching key. + * @return #GNUNET_OK on success, #GNUNET_SYSERR on error */ -static MHD_RESULT -set_token_authority (struct OrderContext *oc, - const char *slug, - struct GNUNET_TIME_Timestamp start_date) +static enum GNUNET_GenericReturnValue +set_token_family (struct OrderContext *oc, + const char *slug, + struct GNUNET_TIME_Timestamp *valid_after) { struct TALER_MERCHANTDB_TokenFamilyKeyDetails key_details; - struct TALER_MerchantContractTokenAuthority authority; + struct TALER_MerchantContractTokenFamily *family = NULL; enum GNUNET_DB_QueryStatus qs; - struct GNUNET_TIME_Absolute min_start_date = GNUNET_TIME_absolute_subtract ( - start_date.abs_time, - // TODO: make this configurable. This is the granularity of token - // expiration dates. - GNUNET_TIME_UNIT_DAYS - ); + /* TODO: Implement rounding duration of token family and use this here. */ + struct GNUNET_TIME_Relative precision = GNUNET_TIME_UNIT_MONTHS; + struct GNUNET_TIME_Timestamp min_valid_after; + struct GNUNET_TIME_Timestamp max_valid_after; + + if ( GNUNET_OK != get_rounded_time_interval (precision, + *valid_after, + &min_valid_after, + &max_valid_after)) + { + GNUNET_break (0); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "valid_after"); + return GNUNET_SYSERR; + } + + for (unsigned int i = 0; i<oc->parse_choices.token_families_len; i++) + { + if (0 == strcmp (oc->parse_choices.token_families[i].slug, + slug)) + { + family = &oc->parse_choices.token_families[i]; + break; + } + } + + if (NULL != family) + { + for (unsigned int i = 0; i<family->keys_len; i++) + { + if (GNUNET_TIME_timestamp_cmp (family->keys[i].valid_after, + >=, + min_valid_after) + && GNUNET_TIME_timestamp_cmp (family->keys[i].valid_after, + <, + max_valid_after)) + { + /* The token family and a matching key is already added. */ + *valid_after = family->keys[i].valid_after; + return GNUNET_OK; + } + } + } qs = TMH_db->lookup_token_family_key (TMH_db->cls, oc->hc->instance->settings.id, slug, - GNUNET_TIME_absolute_to_timestamp ( - min_start_date), - start_date, + min_valid_after, + max_valid_after, &key_details); if (qs <= 0) @@ -1386,85 +1538,164 @@ set_token_authority (struct OrderContext *oc, http_status, ec, "token_family_slug"); - return MHD_NO; + return GNUNET_SYSERR; } - if (NULL == key_details.pub) + struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); + + /* Verify that the token family is valid right now. */ + if (GNUNET_TIME_timestamp_cmp (key_details.token_family.valid_after, >, now) || + GNUNET_TIME_timestamp_cmp (key_details.token_family.valid_before, <=, now)) { - /* If public key is NULL, private key must also be NULL */ - GNUNET_assert (NULL == key_details.priv); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Token family expired or not yet valid\n"); + reply_with_error (oc, + /* TODO: HTTP Status Code GONE would be more elegant, + but that is already used to indicate that a product is out of stock. */ + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_NOT_VALID, + key_details.token_family.slug); + return GNUNET_SYSERR; + } + + /* slug is not needed */ + GNUNET_free (key_details.token_family.slug); - struct GNUNET_CRYPTO_BlindSignPrivateKey *priv; - struct GNUNET_CRYPTO_BlindSignPublicKey *pub; - struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); - struct GNUNET_TIME_Timestamp valid_before = - GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_absolute_add (now, - key_details.token_family.duration)); + { + struct TALER_MerchantContractTokenFamilyKey key; - GNUNET_CRYPTO_blind_sign_keys_create (&priv, - &pub, - // TODO: Make cipher and key length configurable - GNUNET_CRYPTO_BSA_RSA, - 4096); + if (NULL == family) + { + struct TALER_MerchantContractTokenFamily new_family = { + .slug = slug, + .name = key_details.token_family.name, + .description = key_details.token_family.description, + .description_i18n = key_details.token_family.description_i18n, + .keys = GNUNET_new (struct TALER_MerchantContractTokenFamilyKey), + .keys_len = 0, + }; - struct TALER_TokenFamilyPublicKey token_pub = { - .public_key = *pub, - }; - struct TALER_TokenFamilyPrivateKey token_priv = { - .private_key = *priv, - }; + switch (key_details.token_family.kind) { + case TALER_MERCHANTDB_TFK_Subscription: + new_family.kind = TALER_MCTK_SUBSCRIPTION; + new_family.critical = true; + // TODO: Set trusted domains + break; + case TALER_MERCHANTDB_TFK_Discount: + new_family.kind = TALER_MCTK_DISCOUNT; + new_family.critical = false; + // TODO: Set expected domains + break; + } - qs = TMH_db->insert_token_family_key (TMH_db->cls, - slug, - &token_pub, - &token_priv, - GNUNET_TIME_absolute_to_timestamp (now - ), - valid_before); + GNUNET_array_append (oc->parse_choices.token_families, + oc->parse_choices.token_families_len, + new_family); - authority.token_expiration = valid_before; - authority.pub = &token_pub; - // GNUNET_CRYPTO_blind_sign_priv_decref (&token_priv.private_key); - } - else - { - authority.token_expiration = key_details.valid_before; - authority.pub = key_details.pub; - } + family = &oc->parse_choices.token_families[oc->parse_choices.token_families_len - 1]; + } - authority.label = slug; - authority.description = key_details.token_family.description; - authority.description_i18n = key_details.token_family.description_i18n; + if (NULL == key_details.pub.public_key) + { + /* There is no matching key for this token family yet. */ + /* We have to generate a fresh key pair. */ + /* If public key is NULL, private key must also be NULL */ + GNUNET_assert (NULL == key_details.priv.private_key); + + enum GNUNET_DB_QueryStatus iqs; + struct GNUNET_CRYPTO_BlindSignPrivateKey *priv; + struct GNUNET_CRYPTO_BlindSignPublicKey *pub; + struct GNUNET_TIME_Timestamp valid_before = + GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_absolute_add (min_valid_after.abs_time, + key_details.token_family.duration)); + + if (GNUNET_TIME_timestamp_cmp (min_valid_after, + <, + key_details.token_family.valid_after)) + { + /* If key would start before validity of token family, + set valid_after of key to the value of the token family. */ + min_valid_after = key_details.token_family.valid_after; + } - GNUNET_free (key_details.token_family.slug); - GNUNET_free (key_details.token_family.name); - if (NULL != key_details.priv) - { - GNUNET_CRYPTO_blind_sign_priv_decref (&key_details.priv->private_key); - } + if (GNUNET_TIME_timestamp_cmp (valid_before, + >, + key_details.token_family.valid_before)) + { + /* If key would end after validity of token family, + set valid_before of key to the value of the token family. */ + valid_before = key_details.token_family.valid_before; + } - switch (key_details.token_family.kind) - { - case TALER_MERCHANTDB_TFK_Subscription: - authority.kind = TALER_MCTK_SUBSCRIPTION; - authority.details.subscription.start_date = key_details.valid_after; - authority.details.subscription.end_date = key_details.valid_before; - authority.critical = true; - // TODO: Set trusted domains - break; - case TALER_MERCHANTDB_TFK_Discount: - authority.kind = TALER_MCTK_DISCOUNT; - authority.critical = false; - // TODO: Set expected domains - break; - } + GNUNET_CRYPTO_blind_sign_keys_create (&priv, + &pub, + /* TODO: Make cipher and key length configurable */ + GNUNET_CRYPTO_BSA_RSA, + 4096); + + struct TALER_TokenIssuePublicKeyP token_pub = { + .public_key = pub, + }; + struct TALER_TokenIssuePrivateKeyP token_priv = { + .private_key = priv, + }; - GNUNET_array_append (oc->parse_choices.authorities, - oc->parse_choices.authorities_len, - authority); + iqs = TMH_db->insert_token_family_key (TMH_db->cls, + slug, + &token_pub, + &token_priv, + min_valid_after, + valid_before); + + GNUNET_CRYPTO_blind_sign_priv_decref (priv); + + if (iqs <= 0) + { + enum TALER_ErrorCode ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + unsigned int http_status = 0; - return MHD_YES; + switch (iqs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + ec = TALER_EC_GENERIC_DB_STORE_FAILED; + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + ec = TALER_EC_GENERIC_DB_SOFT_FAILURE; + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* case listed to make compilers happy */ + GNUNET_assert (0); + } + + GNUNET_break (0); + reply_with_error (oc, + http_status, + ec, + "token_family_slug"); + return GNUNET_SYSERR; + } + + key.pub = token_pub; + key.valid_after = min_valid_after; + key.valid_before = valid_before; + } else { + key.pub = key_details.pub; + key.valid_after = key_details.valid_after; + key.valid_before = key_details.valid_before; + } + + GNUNET_array_append (family->keys, + family->keys_len, + key); + + *valid_after = key.valid_after; + } + + return GNUNET_OK; } @@ -1481,7 +1712,7 @@ serialize_order (struct OrderContext *oc) const struct TALER_MERCHANTDB_InstanceSettings *settings = &oc->hc->instance->settings; json_t *merchant; - json_t *token_types = json_object (); + json_t *token_families = json_object (); json_t *choices = json_array (); merchant = GNUNET_JSON_PACK ( @@ -1528,38 +1759,54 @@ serialize_order (struct OrderContext *oc) } } - for (unsigned int i = 0; i<oc->parse_choices.authorities_len; i++) + for (unsigned int i = 0; i<oc->parse_choices.token_families_len; i++) { - struct TALER_MerchantContractTokenAuthority *authority = &oc->parse_choices. - authorities[i]; + json_t *keys = json_array(); + struct TALER_MerchantContractTokenFamily *family = &oc->parse_choices.token_families[i]; + + for (unsigned int j = 0; j<family->keys_len; j++) + { + struct TALER_MerchantContractTokenFamilyKey key = family->keys[j]; + + json_t *jkey = GNUNET_JSON_PACK ( + /* TODO: Remove h_pub. */ + GNUNET_JSON_pack_data_auto ("h_pub", + &key.pub.public_key->pub_key_hash), + GNUNET_JSON_pack_allow_null( + GNUNET_JSON_pack_rsa_public_key ("rsa_pub", + key.pub.public_key->details.rsa_public_key)), + // GNUNET_JSON_pack_allow_null( + // GNUNET_JSON_pack_data_auto ("cs_pub", + // &key.pub.public_key->details.cs_public_key)), + GNUNET_JSON_pack_int64 ("cipher", + key.pub.public_key->cipher), + GNUNET_JSON_pack_timestamp ("valid_after", + key.valid_after), + GNUNET_JSON_pack_timestamp ("valid_before", + key.valid_before) + ); - // TODO: Finish spec to clearly define how token families are stored in - // ContractTerms. - // Here are some thoughts: - // - Multiple keys of the same token family can be referenced in - // one contract. E.g. exchanging old subscription for new. - // - TokenAuthority should be renamed to TokenFamily for consistency. - // - TokenFamilySlug can be used instead of TokenAuthorityLabel, but - // every token-based in- or output needs to have a valid_after date, - // so it's clear with key is referenced. - json_t *jauthority = GNUNET_JSON_PACK ( + GNUNET_assert (0 == json_array_append_new (keys, jkey)); + } + + /* TODO: Add 'details' field. */ + json_t *jfamily = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("name", + family->name), GNUNET_JSON_pack_string ("description", - authority->description), + family->description), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_object_incref ("description_i18n", - authority->description_i18n)), - GNUNET_JSON_pack_data_auto ("h_pub", - &authority->pub->public_key.pub_key_hash), - GNUNET_JSON_pack_data_auto ("pub", - &authority->pub->public_key.pub_key_hash), - GNUNET_JSON_pack_timestamp ("token_expiration", - authority->token_expiration) - ); + family->description_i18n)), + GNUNET_JSON_pack_array_steal ("keys", + keys), + GNUNET_JSON_pack_bool ("critical", + family->critical) + ); - GNUNET_assert (0 == - json_object_set_new (token_types, - authority->label, - jauthority)); + GNUNET_assert (0 == json_object_set_new (token_families, + family->slug, + jfamily)); } for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) @@ -1573,25 +1820,19 @@ serialize_order (struct OrderContext *oc) { struct TALER_MerchantContractInput *input = &choice->inputs[j]; - json_t *jinput = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_int64 ("type", - input->type) - ); + /* For now, only tokens are supported */ + GNUNET_assert (TALER_MCIT_TOKEN == input->type); - if (TALER_MCIT_TOKEN == input->type) - { - GNUNET_assert (0 == - json_object_set_new (jinput, - "number", - json_integer ( - input->details.token.count))); - GNUNET_assert (0 == - json_object_set_new (jinput, - "token_family_slug", - json_string ( - input->details.token. - token_family_slug))); - } + json_t *jinput = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("kind", + TMH_string_from_contract_input_type (input->type)), + GNUNET_JSON_pack_string ("token_family_slug", + input->details.token.token_family_slug), + GNUNET_JSON_pack_int64 ("number", + input->details.token.count), + GNUNET_JSON_pack_timestamp ("valid_after", + input->details.token.valid_after) + ); GNUNET_assert (0 == json_array_append_new (inputs, jinput)); } @@ -1600,26 +1841,19 @@ serialize_order (struct OrderContext *oc) { struct TALER_MerchantContractOutput *output = &choice->outputs[j]; - json_t *joutput = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_int64 ("type", - output->type) - ); - - if (TALER_MCOT_TOKEN == output->type) - { - GNUNET_assert (0 == - json_object_set_new (joutput, - "number", - json_integer ( - output->details.token.count))); + /* For now, only tokens are supported */ + GNUNET_assert (TALER_MCOT_TOKEN == output->type); - GNUNET_assert (0 == - json_object_set_new (joutput, - "token_family_slug", - json_string ( - output->details.token. - token_family_slug))); - } + json_t *joutput = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("kind", + TMH_string_from_contract_output_type (output->type)), + GNUNET_JSON_pack_string ("token_family_slug", + output->details.token.token_family_slug), + GNUNET_JSON_pack_int64 ("number", + output->details.token.count), + GNUNET_JSON_pack_timestamp ("valid_after", + output->details.token.valid_after) + ); GNUNET_assert (0 == json_array_append (outputs, joutput)); } @@ -1691,13 +1925,13 @@ serialize_order (struct OrderContext *oc) TALER_JSON_pack_amount ("amount", &oc->parse_order.brutto), GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_array_incref ("choices", - choices) - ), + GNUNET_JSON_pack_array_steal ("choices", + choices) + ), GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("token_types", - token_types) - ), + GNUNET_JSON_pack_object_steal ("token_families", + token_families) + ), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_object_incref ("extra", (json_t *) oc->parse_order.extra)) @@ -1780,8 +2014,8 @@ set_max_fee (struct OrderContext *oc) static bool set_exchanges (struct OrderContext *oc) { - /* Note: re-building 'oc->exchanges' every time here might be a tad - expensive; could likely consider caching the result if it starts to + /* Note: re-building 'oc->set_exchanges.exchanges' every time here might be a + tad expensive; could likely consider caching the result if it starts to matter. */ if (NULL == oc->set_exchanges.exchanges) { @@ -1852,6 +2086,8 @@ parse_order (struct OrderContext *oc) * mostly because in GNUnet relative times can't * be negative. */ bool no_fee; + /* TODO: Move "amount" field to choices. This entails moving the + stefan-base fee calculation to the parse_choices phase. */ struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("version", @@ -2314,10 +2550,14 @@ parse_choices (struct OrderContext *oc) const json_t *jinputs; const json_t *joutputs; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_array_const ("inputs", - &jinputs), - GNUNET_JSON_spec_array_const ("outputs", - &joutputs), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("inputs", + &jinputs), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("outputs", + &joutputs), + NULL), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue ret; @@ -2339,14 +2579,15 @@ parse_choices (struct OrderContext *oc) return; } - if (! json_is_array (jinputs) || - ! json_is_array (joutputs)) + if (0 == json_array_size(jinputs) && 0 == json_array_size(joutputs)) { - GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Choice #%u must have at least one input or output\n", + i); reply_with_error (oc, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, - "inputs or outputs"); + "choice"); return; } @@ -2356,10 +2597,6 @@ parse_choices (struct OrderContext *oc) size_t idx; json_array_foreach ((json_t *) jinputs, idx, jinput) { - // TODO: Assuming this struct would have more fields, would i use GNUNET_new then? - // Or should i use GNUNET_grow first and then get the element using the index? - // Assuming you add a field in the future, it's easier to that way, so you don't - // free it. struct TALER_MerchantContractInput input = {.details.token.count = 1}; const char *kind; const char *ierror_name; @@ -2370,6 +2607,8 @@ parse_choices (struct OrderContext *oc) &kind), GNUNET_JSON_spec_string ("token_family_slug", &input.details.token.token_family_slug), + /* TODO: Remove valid_after field for inputs, + use current system time instead. */ GNUNET_JSON_spec_timestamp ("valid_after", &input.details.token.valid_after), GNUNET_JSON_spec_mark_optional ( @@ -2396,7 +2635,7 @@ parse_choices (struct OrderContext *oc) return; } - input.type = TMH_string_to_contract_input_type (kind); + input.type = TMH_contract_input_type_from_string (kind); if (TALER_MCIT_INVALID == input.type) { @@ -2416,29 +2655,12 @@ parse_choices (struct OrderContext *oc) continue; } - bool found = false; - - for (unsigned int i = 0; i<oc->parse_choices.authorities_len; i++) + if (GNUNET_OK != set_token_family (oc, + input.details.token.token_family_slug, + &input.details.token.valid_after)) { - if (0 == strcmp (oc->parse_choices.authorities[i].label, - input.details.token.token_family_slug)) - { - found = true; - break; - } - } - - if (! found) - { - MHD_RESULT res; - res = set_token_authority (oc, - input.details.token.token_family_slug, - input.details.token.valid_after); - - if (MHD_NO == res) - { - return; - } + /* error is already scheduled, return. */ + return; } GNUNET_array_append (oc->parse_choices.choices[i].inputs, @@ -2463,6 +2685,7 @@ parse_choices (struct OrderContext *oc) &kind), GNUNET_JSON_spec_string ("token_family_slug", &output.details.token.token_family_slug), + /* TODO: Make valid_after optional, default to current system time. */ GNUNET_JSON_spec_timestamp ("valid_after", &output.details.token.valid_after), GNUNET_JSON_spec_mark_optional ( @@ -2491,7 +2714,7 @@ parse_choices (struct OrderContext *oc) return; } - output.type = TMH_string_to_contract_output_type (kind); + output.type = TMH_contract_output_type_from_string (kind); if (TALER_MCOT_INVALID == output.type) { @@ -2509,33 +2732,16 @@ parse_choices (struct OrderContext *oc) if (0 == output.details.token.count) { - /* Ignore outputs with 'number' field set to 0 */ + /* Ignore outputs with 'number' field set to 0. */ continue; } - bool found = false; - - for (unsigned int i = 0; i<oc->parse_choices.authorities_len; i++) + if (GNUNET_OK != set_token_family (oc, + output.details.token.token_family_slug, + &output.details.token.valid_after)) { - if (0 == strcmp (oc->parse_choices.authorities[i].label, - output.details.token.token_family_slug)) - { - found = true; - break; - } - } - - if (! found) - { - MHD_RESULT res; - res = set_token_authority (oc, - output.details.token.token_family_slug, - output.details.token.valid_after); - - if (MHD_NO == res) - { - return; - } + /* Error is already scheduled, return. */ + return; } GNUNET_array_append (oc->parse_choices.choices[i].outputs, @@ -2651,7 +2857,6 @@ merge_inventory (struct OrderContext *oc) /* case listed to make compilers happy */ GNUNET_assert (0); } - json_decref (oc->merge_inventory.products); reply_with_error (oc, http_status, ec, diff --git a/src/backend/taler-merchant-httpd_private-post-token-families.c b/src/backend/taler-merchant-httpd_private-post-token-families.c index f4472c39..5a342af2 100644 --- a/src/backend/taler-merchant-httpd_private-post-token-families.c +++ b/src/backend/taler-merchant-httpd_private-post-token-families.c @@ -25,6 +25,7 @@ #include "platform.h" #include "taler-merchant-httpd_private-post-token-families.h" #include "taler-merchant-httpd_helper.h" +#include <gnunet/gnunet_time_lib.h> #include <taler/taler_json_lib.h> @@ -74,6 +75,7 @@ TMH_private_post_token_families (const struct TMH_RequestHandler *rh, struct TMH_MerchantInstance *mi = hc->instance; struct TALER_MERCHANTDB_TokenFamilyDetails details = { 0 }; const char *kind = NULL; + bool no_valid_after = false; enum GNUNET_DB_QueryStatus qs; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("slug", @@ -87,8 +89,10 @@ TMH_private_post_token_families (const struct TMH_RequestHandler *rh, &details.description_i18n), NULL), GNUNET_JSON_spec_string ("kind", &kind), - GNUNET_JSON_spec_timestamp ("valid_after", - &details.valid_after), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("valid_after", + &details.valid_after), + &no_valid_after), GNUNET_JSON_spec_timestamp ("valid_before", &details.valid_before), GNUNET_JSON_spec_relative_time ("duration", @@ -112,6 +116,33 @@ TMH_private_post_token_families (const struct TMH_RequestHandler *rh, } } + struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); + + if (no_valid_after) { + details.valid_after = now; + } + + /* Ensure that valid_after is before valid_before */ + if (GNUNET_TIME_timestamp_cmp (details.valid_after, >=, details.valid_before)) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "valid_before"); + } + + /* Ensure that valid_after is not in the past */ + if (GNUNET_TIME_timestamp_cmp (details.valid_after, <, now)) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "valid_after"); + } if (strcmp (kind, "discount") == 0) details.kind = TALER_MERCHANTDB_TFK_Discount; |