commit 4d6092453dbce9b943019a3accf280b09fa2593d
parent d2d7c542df491ff82f594b39f6ffd47613389d3a
Author: Christian Grothoff <christian@grothoff.org>
Date: Thu, 6 Aug 2026 23:18:03 +0200
replace badly named cur_time with expiration
Diffstat:
6 files changed, 173 insertions(+), 50 deletions(-)
diff --git a/README b/README
@@ -36,6 +36,10 @@ How it works
with a reference to the unique payment identifier.
6. `paivana-httpd` verifies the payment with the merchant, sets
an HMAC access cookie, and redirects the browser to the original URL.
+ The access ends at the `expiration` the browser named in step 3 and
+ repeated in step 5 — it is hashed into the payment identifier, so the
+ two must agree — bounded above by the contract's `max_pickup_time`,
+ which is what the merchant's `max_pickup_delay` on the template sets.
7. Requests with a valid cookie are forwarded to the upstream server
via libcurl and the response is streamed back.
diff --git a/src/backend/paivana-httpd_cookie.c b/src/backend/paivana-httpd_cookie.c
@@ -39,17 +39,17 @@ struct GNUNET_HashCode paivana_secret;
/**
- * Compute access cookie hash for the given @a cur_time, the
+ * Compute access cookie hash for the given @a expiration, the
* @a website and @a ca.
*
- * @param cur_time current time used in the cookie
+ * @param expiration time at which the access being granted ends
* @param website URL the cookie is valid for
* @param ca_len number of bytes in @a ca
* @param ca client (IP) address
* @param[out] c set to the cookie hash
*/
static void
-compute_cookie_hash (struct GNUNET_TIME_Timestamp cur_time,
+compute_cookie_hash (struct GNUNET_TIME_Timestamp expiration,
const char *website,
size_t ca_len,
const void *ca,
@@ -58,12 +58,12 @@ compute_cookie_hash (struct GNUNET_TIME_Timestamp cur_time,
struct GNUNET_TIME_AbsoluteNBO e;
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
- "Computing cookie for %s at %llu and client %s\n",
+ "Computing cookie for %s expiring at %llu and client %s\n",
website,
- (unsigned long long) cur_time.abs_time.abs_value_us,
+ (unsigned long long) expiration.abs_time.abs_value_us,
TALER_b2s (ca,
ca_len));
- e = GNUNET_TIME_absolute_hton (cur_time.abs_time);
+ e = GNUNET_TIME_absolute_hton (expiration.abs_time);
if (PH_global_cookie)
website = "";
GNUNET_assert (GNUNET_YES ==
@@ -259,7 +259,7 @@ encode_path (const char *path)
char *
-PAIVANA_HTTPD_compute_cookie (struct GNUNET_TIME_Timestamp cur_time,
+PAIVANA_HTTPD_compute_cookie (struct GNUNET_TIME_Timestamp expiration,
const char *website,
size_t ca_len,
const void *ca)
@@ -292,7 +292,17 @@ PAIVANA_HTTPD_compute_cookie (struct GNUNET_TIME_Timestamp cur_time,
"https://",
strlen ("https://")));
struct GNUNET_TIME_Relative duration
- = GNUNET_TIME_absolute_get_remaining (cur_time.abs_time);
+ = GNUNET_TIME_absolute_get_remaining (expiration.abs_time);
+ /* RFC 6265 section 5.2.2: a non-positive Max-Age tells the user agent
+ to expire the cookie immediately. Truncating to seconds turns any
+ lifetime below one second into exactly that, so the client would
+ pay and have the cookie deleted on arrival; give it the one second
+ the wire format is able to express instead. */
+ unsigned long long max_age
+ = GNUNET_MAX (1LLU,
+ (unsigned long long) (duration.rel_value_us
+ / GNUNET_TIME_UNIT_SECONDS.
+ rel_value_us));
if (! PH_global_cookie)
{
@@ -315,7 +325,7 @@ PAIVANA_HTTPD_compute_cookie (struct GNUNET_TIME_Timestamp cur_time,
website);
}
}
- compute_cookie_hash (cur_time,
+ compute_cookie_hash (expiration,
website,
ca_len,
ca,
@@ -333,20 +343,20 @@ PAIVANA_HTTPD_compute_cookie (struct GNUNET_TIME_Timestamp cur_time,
-- every ';' is followed by SP and an attribute, so the value
must not end on one. */
PAIVANA_COOKIE_NAME "=%llu-%s; %sPath=%s; Max-Age=%llu; HttpOnly",
- (unsigned long long) (cur_time.abs_time.abs_value_us / 1000LLU / 1000LLU),
+ (unsigned long long) (expiration.abs_time.abs_value_us / 1000LLU / 1000LLU),
cstr,
use_https
? "Secure; "
: "",
url,
- (unsigned long long) (duration.rel_value_us / 1000 / 1000));
+ max_age);
GNUNET_free (epath);
return res;
}
char *
-PAIVANA_HTTPD_compute_paivana_id (struct GNUNET_TIME_Timestamp cur_time,
+PAIVANA_HTTPD_compute_paivana_id (struct GNUNET_TIME_Timestamp expiration,
const char *website,
const struct PAIVANA_Nonce *nonce)
{
@@ -357,7 +367,7 @@ PAIVANA_HTTPD_compute_paivana_id (struct GNUNET_TIME_Timestamp cur_time,
char *cstr;
size_t clen;
- e = GNUNET_TIME_absolute_hton (cur_time.abs_time);
+ e = GNUNET_TIME_absolute_hton (expiration.abs_time);
GNUNET_assert (0 ==
gcry_md_open (&hd,
GCRY_MD_SHA256,
@@ -380,7 +390,7 @@ PAIVANA_HTTPD_compute_paivana_id (struct GNUNET_TIME_Timestamp cur_time,
GNUNET_asprintf (
&res,
"%llu-%.*s",
- (unsigned long long) (cur_time.abs_time.abs_value_us / 1000LLU / 1000LLU),
+ (unsigned long long) (expiration.abs_time.abs_value_us / 1000LLU / 1000LLU),
(int) clen,
cstr);
GNUNET_free (cstr);
diff --git a/src/backend/paivana-httpd_cookie.h b/src/backend/paivana-httpd_cookie.h
@@ -71,7 +71,7 @@ PAIVANA_HTTPD_check_cookie (const char *cookie,
/**
* Compute the `Set-Cookie` line granting access to @a website until
- * @a cur_time for a client at @a ca.
+ * @a expiration for a client at @a ca.
*
* The `Path` attribute is scoped to @a website (unless
* #PH_global_cookie), percent-encoded so that it path-matches the
@@ -79,30 +79,31 @@ PAIVANA_HTTPD_check_cookie (const char *cookie,
* falls back to "/" for a path that RFC 6265 section 4.1.1 cannot
* express. `Secure` follows #PH_base_url where that is configured.
*
- * @param cur_time expiration time of the cookie
+ * @param expiration expiration time of the cookie
* @param website URL of the site the cookie is for
* @param ca_len number of bytes in @a ca
* @param ca client address
* @return the value for the `Set-Cookie` header; the caller must free
*/
char *
-PAIVANA_HTTPD_compute_cookie (struct GNUNET_TIME_Timestamp cur_time,
+PAIVANA_HTTPD_compute_cookie (struct GNUNET_TIME_Timestamp expiration,
const char *website,
size_t ca_len,
const void *ca);
/**
- * Compute the Paivana ID for the given @a cur_time,
+ * Compute the Paivana ID for the given @a expiration,
* @a website and @a nonce.
*
- * @param cur_time time chosen (by client)
+ * @param expiration end of access, as chosen by the client and bounded
+ * by the contract's `max_pickup_time`
* @param website website to be accessed
* @param nonce client-selected nonce
* @return corresponding Paivana ID.
*/
char *
-PAIVANA_HTTPD_compute_paivana_id (struct GNUNET_TIME_Timestamp cur_time,
+PAIVANA_HTTPD_compute_paivana_id (struct GNUNET_TIME_Timestamp expiration,
const char *website,
const struct PAIVANA_Nonce *nonce);
diff --git a/src/backend/paivana-httpd_pay.c b/src/backend/paivana-httpd_pay.c
@@ -37,15 +37,6 @@ struct PayRequest;
#include "taler/merchant/get-private-orders-ORDER_ID.h"
/**
- * How much do we allow the client clock to be off and still
- * accept the client's timestamp? This basically allows a
- * client to extend the lifetime of their purchase by putting
- * the payment date into the future.
- */
-#define CLIENT_PAY_TIME_TOLERANCE \
- GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 90)
-
-/**
* Handle for processing actual payment.
*/
struct PayRequest
@@ -105,9 +96,12 @@ struct PayRequest
struct PAIVANA_Nonce nonce;
/**
- * Expiration time of the cookie.
+ * End of the access the client is redeeming: the expiration of the
+ * cookie we mint, and one of the three inputs the client hashed into
+ * the paivana_id the order was created under. Chosen by the client
+ * and bounded above by the contract's `max_pickup_time'.
*/
- struct GNUNET_TIME_Timestamp cur_time;
+ struct GNUNET_TIME_Timestamp expiration;
/**
* HTTP status to return in combination with @e resp to the client.
@@ -284,7 +278,7 @@ check_contract (struct PayRequest *ph,
ph->response_status = MHD_HTTP_CONFLICT;
return false;
}
- if (GNUNET_TIME_timestamp_cmp (ph->cur_time,
+ if (GNUNET_TIME_timestamp_cmp (ph->expiration,
>,
max_time))
{
@@ -361,7 +355,7 @@ order_status_cb (struct PayRequest *ph,
ph->response_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
break;
}
- cookie = PAIVANA_HTTPD_compute_cookie (ph->cur_time,
+ cookie = PAIVANA_HTTPD_compute_cookie (ph->expiration,
ph->website,
ca_len,
ca);
@@ -452,8 +446,8 @@ PAIVANA_HTTPD_payment_handle (struct PayRequest *ph,
&ph->order_id),
TALER_JSON_spec_web_url ("website",
&ph->website),
- GNUNET_JSON_spec_timestamp ("cur_time",
- &ph->cur_time),
+ GNUNET_JSON_spec_timestamp ("expiration",
+ &ph->expiration),
GNUNET_JSON_spec_fixed_auto ("nonce",
&ph->nonce),
GNUNET_JSON_spec_end ()
@@ -465,17 +459,22 @@ PAIVANA_HTTPD_payment_handle (struct PayRequest *ph,
spec);
if (GNUNET_YES != ret)
return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
- if (GNUNET_TIME_relative_cmp (
- GNUNET_TIME_absolute_get_remaining (ph->cur_time.abs_time),
- >,
- CLIENT_PAY_TIME_TOLERANCE))
+ /* `expiration' is the end of the access being bought, not a
+ statement about the client's clock: it is what the cookie's
+ Max-Age is computed from, what check_cookie() enforces, and what
+ is hashed into the paivana_id the order was created under, so we
+ cannot re-derive it here even if we wanted to. An expiration in
+ the past would mint a cookie that is dead on arrival; the upper
+ bound is the contract's `max_pickup_time', enforced in
+ check_contract() once we have the contract to compare against. */
+ if (GNUNET_TIME_absolute_is_past (ph->expiration.abs_time))
{
- GNUNET_break (0);
+ GNUNET_break_op (0);
return TALER_MHD_reply_with_error (
ph->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
- "cur_time");
+ "expiration");
}
}
GNUNET_assert (NULL == ph->co);
@@ -493,7 +492,7 @@ PAIVANA_HTTPD_payment_handle (struct PayRequest *ph,
{
char *paivana_id;
- paivana_id = PAIVANA_HTTPD_compute_paivana_id (ph->cur_time,
+ paivana_id = PAIVANA_HTTPD_compute_paivana_id (ph->expiration,
ph->website,
&ph->nonce);
GNUNET_assert (
diff --git a/src/frontend/paywall.js b/src/frontend/paywall.js
@@ -128,7 +128,7 @@ async function confirmPayment(order_id, linkEl, errorEl, nonce) {
body: JSON.stringify({
order_id,
nonce,
- cur_time: { t_s: expTime },
+ expiration: { t_s: expTime },
website,
}),
});
diff --git a/src/tests/test_cookie_header.c b/src/tests/test_cookie_header.c
@@ -21,7 +21,7 @@
/**
* @file test_cookie_header.c
* @brief tests the `Set-Cookie` line paivana emits for the access
- * cookie: its `Path` attribute and its `Secure` attribute
+ * cookie: its `Path`, `Secure` and `Max-Age` attributes
*
* The cookie is the credential the client just paid for, and both
* attributes decide whether it ever comes back:
@@ -35,6 +35,8 @@
* section 4.1.1) admits neither ';' nor CTLs, both of which a URI
* path may legitimately carry.
* - `Secure` decides whether the credential may travel in the clear.
+ * - `Max-Age` decides how long it lives, and a value of zero (RFC 6265
+ * section 5.2.2) deletes it on arrival.
*
* The integration suite cannot cover any of this: it runs paivana with
* -n, where the cookie path is never reached at all.
@@ -58,6 +60,27 @@ static unsigned int failures;
/**
+ * Compute a `Set-Cookie` line granting access to @a website until
+ * @a expiration, from a fixed client address.
+ *
+ * @param website URL the cookie is minted for
+ * @param expiration end of the access being granted
+ * @return the header value, to be freed by the caller
+ */
+static char *
+set_cookie_until (const char *website,
+ struct GNUNET_TIME_Timestamp expiration)
+{
+ static const uint8_t ca[4] = { 203, 0, 113, 7 };
+
+ return PAIVANA_HTTPD_compute_cookie (expiration,
+ website,
+ sizeof (ca),
+ ca);
+}
+
+
+/**
* Compute a `Set-Cookie` line for @a website with an expiration an
* hour out and a fixed client address.
*
@@ -67,13 +90,9 @@ static unsigned int failures;
static char *
set_cookie (const char *website)
{
- static const uint8_t ca[4] = { 203, 0, 113, 7 };
-
- return PAIVANA_HTTPD_compute_cookie (
- GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS),
+ return set_cookie_until (
website,
- sizeof (ca),
- ca);
+ GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS));
}
@@ -112,6 +131,71 @@ attribute (const char *sc,
/**
+ * Check that a cookie expiring at @a expiration carries a `Max-Age`
+ * between @a lo and @a hi inclusive.
+ *
+ * The bounds are a range because the lifetime is measured from the
+ * moment of the call while @a expiration is second-granular, so an
+ * hour out is 3599 or 3600 seconds depending on where in the current
+ * second we happen to be.
+ *
+ * @param label what the case is called in the log
+ * @param expiration end of the access being granted
+ * @param lo smallest acceptable `Max-Age`
+ * @param hi largest acceptable `Max-Age`
+ */
+static void
+max_age_between (const char *label,
+ struct GNUNET_TIME_Timestamp expiration,
+ unsigned long long lo,
+ unsigned long long hi)
+{
+ const char *website = "http://example.com/premium/article";
+ char *sc;
+ char *got;
+ unsigned long long ma;
+
+ sc = set_cookie_until (website,
+ expiration);
+ got = attribute (sc,
+ "Max-Age");
+ if ( (NULL == got) ||
+ (1 != sscanf (got,
+ "%llu",
+ &ma)) )
+ {
+ fprintf (stderr,
+ "FAIL: %s gives Max-Age=%s, want a number in [%llu,%llu]\n",
+ label,
+ (NULL != got) ? got : "(none)",
+ lo,
+ hi);
+ failures++;
+ }
+ else if ( (ma < lo) ||
+ (ma > hi) )
+ {
+ fprintf (stderr,
+ "FAIL: %s gives Max-Age=%llu, want [%llu,%llu]\n",
+ label,
+ ma,
+ lo,
+ hi);
+ failures++;
+ }
+ else
+ {
+ fprintf (stderr,
+ " ok: %s -> Max-Age=%llu\n",
+ label,
+ ma);
+ }
+ GNUNET_free (got);
+ GNUNET_free (sc);
+}
+
+
+/**
* Is the attribute @a name (one without a value, such as `Secure`)
* present in the `Set-Cookie` line @a sc?
*
@@ -463,6 +547,31 @@ main (int argc,
PH_base_url = NULL;
fprintf (stderr,
+ "-- Max-Age --\n");
+ /* An expiration is second-granular, so one observed from the middle
+ of the preceding second leaves under a second of lifetime. RFC
+ 6265 section 5.2.2 reads a Max-Age of zero as "expire this cookie
+ immediately", which would delete the access the client just paid
+ for on arrival; the smallest lifetime the attribute can express is
+ one second. */
+ max_age_between ("the next whole second",
+ GNUNET_TIME_absolute_to_timestamp (
+ GNUNET_TIME_relative_to_absolute (
+ GNUNET_TIME_UNIT_SECONDS)),
+ 1,
+ 1);
+ max_age_between ("an hour out",
+ GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS),
+ 3599,
+ 3600);
+ /* 'forever' is the default max_pickup_delay, so it is the ordinary
+ case rather than an edge one. */
+ max_age_between ("forever",
+ GNUNET_TIME_UNIT_FOREVER_TS,
+ 365ULL * 24 * 60 * 60,
+ ULLONG_MAX);
+
+ fprintf (stderr,
"-- the cookie value still verifies --\n");
round_trips ("http://example.com/premium/my article");
round_trips ("http://example.com/a;b");