paivana

HTTP paywall reverse proxy
Log | Files | Refs | Submodules | README | LICENSE

test_cookie_access.c (23011B)


      1 /*
      2      This file is part of GNUnet.
      3      Copyright (C) 2026 Taler Systems SA
      4 
      5      Paivana is free software; you can redistribute it and/or
      6      modify it under the terms of the GNU Affero General Public License
      7      as published by the Free Software Foundation; either version
      8      3, or (at your option) any later version.
      9 
     10      Paivana is distributed in the hope that it will be useful,
     11      but WITHOUT ANY WARRANTY; without even the implied warranty
     12      of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
     13      the GNU Affero General Public License for more details.
     14 
     15      You should have received a copy of the GNU Affero General Public
     16      License along with Paivana; see the file COPYING.  If not,
     17      write to the Free Software Foundation, Inc., 51 Franklin
     18      Street, Fifth Floor, Boston, MA 02110-1301, USA.
     19 */
     20 
     21 /**
     22  * @file test_cookie_access.c
     23  * @brief tests the access decision itself: which cookie values
     24  *        PAIVANA_HTTPD_check_cookie() grants access on, and the
     25  *        `paivana_id` the order is created under
     26  *
     27  * Its sibling test_cookie_header.c covers the `Set-Cookie` line paivana
     28  * emits -- whether the credential ever comes back.  This one covers
     29  * what happens when it does, which is the security boundary: the cookie
     30  * is a bearer token minted by us and handed to the party with the
     31  * strongest possible interest in widening it, so every one of
     32  *
     33  *   - the expiration it was minted for,
     34  *   - the website it was minted for,
     35  *   - the client address it was minted for
     36  *
     37  * has to be inside the MAC and has to be re-checked, and every way a
     38  * value can be malformed has to end in a rejection rather than in a
     39  * read of something that is not there.  None of this is
     40  * reachable from the integration suite: that runs paivana with -n,
     41  * where do_forward is set before the request is even looked at and the
     42  * cookie path is never entered.
     43  *
     44  * `expiration' is the subject of the second half.  It used to be called
     45  * `cur_time' and was read in two mutually exclusive ways at once -- as
     46  * the client's clock, bounded to 90 seconds ahead, and as the end of
     47  * the access being sold, which is what the cookie's Max-Age, this
     48  * file's time bound and the contract's max_pickup_time all treat it as.
     49  * The cases below pin the surviving reading: a cookie minted for an
     50  * hour out is good for an hour (under the other reading it could not be
     51  * minted at all), one minted for a moment already past is good for
     52  * nothing, and moving the expiration in the value breaks the MAC.
     53  */
     54 #include "platform.h"
     55 #include <gnunet/gnunet_util_lib.h>
     56 #include "paivana-httpd_cookie.h"
     57 
     58 /**
     59  * Globals that paivana-httpd.c normally defines; the cookie
     60  * compilation unit references them.
     61  */
     62 int PH_global_cookie;
     63 char *PH_base_url;
     64 
     65 /**
     66  * Number of checks that did not hold.
     67  */
     68 static unsigned int failures;
     69 
     70 /**
     71  * The website most cases are about.
     72  */
     73 #define WEBSITE "https://example.com/premium/article"
     74 
     75 /**
     76  * A client address, in the shape get_client_address() hands over for
     77  * an IPv4 peer: the bare address bytes.
     78  */
     79 static const uint8_t ca4[4] = { 203, 0, 113, 7 };
     80 
     81 /**
     82  * A different client address of the same length.
     83  */
     84 static const uint8_t ca4b[4] = { 203, 0, 113, 8 };
     85 
     86 /**
     87  * An IPv6 client address, i.e. one of a different length.
     88  */
     89 static const uint8_t ca6[16] = {
     90   0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x07
     91 };
     92 
     93 
     94 /**
     95  * Mint an access cookie and return its value, i.e. the `Set-Cookie`
     96  * line with the name and the attributes cut away -- what the browser
     97  * will send back in a `Cookie` header and what check_cookie() is
     98  * handed.
     99  *
    100  * @param website URL the cookie is minted for
    101  * @param expiration end of the access being granted
    102  * @param ca_len number of bytes in @a ca
    103  * @param ca client address the cookie is minted for
    104  * @return the cookie value, to be freed by the caller
    105  */
    106 static char *
    107 mint (const char *website,
    108       struct GNUNET_TIME_Timestamp expiration,
    109       size_t ca_len,
    110       const void *ca)
    111 {
    112   char *sc;
    113   char *eq;
    114   char *semi;
    115   char *val;
    116 
    117   sc = PAIVANA_HTTPD_compute_cookie (expiration,
    118                                      website,
    119                                      ca_len,
    120                                      ca);
    121   eq = strchr (sc,
    122                '=');
    123   GNUNET_assert (NULL != eq);
    124   semi = strchr (eq,
    125                  ';');
    126   if (NULL != semi)
    127     *semi = '\0';
    128   val = GNUNET_strdup (eq + 1);
    129   GNUNET_free (sc);
    130   return val;
    131 }
    132 
    133 
    134 /**
    135  * Mint an access cookie for #WEBSITE, an hour out, for #ca4.
    136  *
    137  * @return the cookie value, to be freed by the caller
    138  */
    139 static char *
    140 mint_default (void)
    141 {
    142   return mint (WEBSITE,
    143                GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS),
    144                sizeof (ca4),
    145                ca4);
    146 }
    147 
    148 
    149 /**
    150  * Check that @a value is (or is not) accepted for @a website and @a ca.
    151  *
    152  * @param label what the case is called in the log
    153  * @param value cookie value to present
    154  * @param website URL the access is claimed for
    155  * @param ca_len number of bytes in @a ca
    156  * @param ca client address the access is claimed from
    157  * @param want true if the cookie is expected to grant access
    158  */
    159 static void
    160 grants (const char *label,
    161         const char *value,
    162         const char *website,
    163         size_t ca_len,
    164         const void *ca,
    165         bool want)
    166 {
    167   bool got;
    168 
    169   got = PAIVANA_HTTPD_check_cookie (value,
    170                                     website,
    171                                     ca_len,
    172                                     ca);
    173   if (got != want)
    174   {
    175     fprintf (stderr,
    176              "FAIL: %s -> %s, want %s\n",
    177              label,
    178              got ? "granted" : "refused",
    179              want ? "granted" : "refused");
    180     failures++;
    181   }
    182   else
    183   {
    184     fprintf (stderr,
    185              "  ok: %s -> %s\n",
    186              label,
    187              got ? "granted" : "refused");
    188   }
    189 }
    190 
    191 
    192 /**
    193  * Check that @a value is refused, whatever it is.  Used for the
    194  * malformed cases, where the point is that nothing is read past the
    195  * end of a value the client chose.
    196  *
    197  * @param label what the case is called in the log
    198  * @param value cookie value to present
    199  */
    200 static void
    201 refuses (const char *label,
    202          const char *value)
    203 {
    204   grants (label,
    205           value,
    206           WEBSITE,
    207           sizeof (ca4),
    208           ca4,
    209           false);
    210 }
    211 
    212 
    213 /**
    214  * Check that @a id has the shape the paywall page and the merchant
    215  * both expect: decimal seconds, '-', then unpadded RFC 4648 section 5
    216  * base64url.  It travels as a merchant `session_id` and, before that,
    217  * through a URL.
    218  *
    219  * @param label what the case is called in the log
    220  * @param id the identifier to inspect
    221  */
    222 static void
    223 id_is_wellformed (const char *label,
    224                   const char *id)
    225 {
    226   const char *dash;
    227 
    228   dash = strchr (id,
    229                  '-');
    230   if ( (NULL == dash) ||
    231        (dash == id) )
    232   {
    233     fprintf (stderr,
    234              "FAIL: %s: `%s' has no seconds prefix\n",
    235              label,
    236              id);
    237     failures++;
    238     return;
    239   }
    240   for (const char *p = id; p < dash; p++)
    241   {
    242     if (! isdigit ((unsigned char) *p))
    243     {
    244       fprintf (stderr,
    245                "FAIL: %s: `%s' has a non-digit in its seconds prefix\n",
    246                label,
    247                id);
    248       failures++;
    249       return;
    250     }
    251   }
    252   for (const char *p = dash + 1; '\0' != *p; p++)
    253   {
    254     if ( (isalnum ((unsigned char) *p)) ||
    255          ('-' == *p) ||
    256          ('_' == *p) )
    257       continue;
    258     fprintf (stderr,
    259              "FAIL: %s: `%s' carries `%c', which is not in the base64url"
    260              " alphabet\n",
    261              label,
    262              id,
    263              *p);
    264     failures++;
    265     return;
    266   }
    267   fprintf (stderr,
    268            "  ok: %s -> %s\n",
    269            label,
    270            id);
    271 }
    272 
    273 
    274 /**
    275  * Check that @a a and @a b differ.
    276  *
    277  * @param label what the case is called in the log
    278  * @param a first identifier
    279  * @param b second identifier
    280  */
    281 static void
    282 differ (const char *label,
    283         const char *a,
    284         const char *b)
    285 {
    286   if (0 == strcmp (a,
    287                    b))
    288   {
    289     fprintf (stderr,
    290              "FAIL: %s: both inputs give `%s'\n",
    291              label,
    292              a);
    293     failures++;
    294   }
    295   else
    296   {
    297     fprintf (stderr,
    298              "  ok: %s\n",
    299              label);
    300   }
    301 }
    302 
    303 
    304 int
    305 main (int argc,
    306       char *const *argv)
    307 {
    308   struct GNUNET_TIME_Timestamp hour;
    309   struct GNUNET_TIME_Timestamp past;
    310   char *val;
    311 
    312   (void) argc;
    313   (void) argv;
    314   /* Quiet: every refusal below logs by design. */
    315   GNUNET_assert (GNUNET_OK ==
    316                  GNUNET_log_setup ("test-cookie-access",
    317                                    "ERROR",
    318                                    NULL));
    319   GNUNET_CRYPTO_hash ("test-cookie-access",
    320                       strlen ("test-cookie-access"),
    321                       &paivana_secret);
    322   hour = GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_HOURS);
    323   /* A fixed instant in the past (2023-11-14), so that the expiry cases
    324      do not depend on how long the test takes to run. */
    325   past.abs_time = GNUNET_TIME_absolute_from_s (1700000000);
    326 
    327   fprintf (stderr,
    328            "-- a cookie grants what it was minted for --\n");
    329   val = mint_default ();
    330   grants ("the cookie just minted",
    331           val,
    332           WEBSITE,
    333           sizeof (ca4),
    334           ca4,
    335           true);
    336   GNUNET_free (val);
    337 
    338   fprintf (stderr,
    339            "-- ...and expires when it says it does --\n");
    340   /* An hour is an ordinary access duration and, before `expiration'
    341      was named that, an unreachable one: the pay endpoint refused any
    342      value more than 90 seconds ahead, so this cookie could not be
    343      obtained at all. */
    344   val = mint (WEBSITE,
    345               hour,
    346               sizeof (ca4),
    347               ca4);
    348   grants ("an expiration an hour out",
    349           val,
    350           WEBSITE,
    351           sizeof (ca4),
    352           ca4,
    353           true);
    354   GNUNET_free (val);
    355   /* compute_cookie() raises a sub-second Max-Age to 1, since a Max-Age
    356      of 0 would delete the cookie on arrival (RFC 6265 section 5.2.2).
    357      That is a floor on the attribute, not on the access: the value
    358      itself still carries the expiration, and this side must not honour
    359      a second the other side rounded up to. */
    360   val = mint (WEBSITE,
    361               past,
    362               sizeof (ca4),
    363               ca4);
    364   grants ("an expiration in the past",
    365           val,
    366           WEBSITE,
    367           sizeof (ca4),
    368           ca4,
    369           false);
    370   GNUNET_free (val);
    371 
    372   fprintf (stderr,
    373            "-- the expiration is inside the MAC --\n");
    374   {
    375     unsigned long long secs;
    376     const char *dash;
    377     char *forged;
    378 
    379     /* The holder of a cookie can read its expiration off the value and
    380        would like a later one.  Rewriting it has to fail: the timestamp
    381        is the KDF salt, so a value whose prefix no longer matches what
    382        was signed cannot verify. */
    383     val = mint_default ();
    384     GNUNET_assert (1 ==
    385                    sscanf (val,
    386                            "%llu-",
    387                            &secs));
    388     dash = strchr (val,
    389                    '-');
    390     GNUNET_assert (NULL != dash);
    391     GNUNET_asprintf (&forged,
    392                      "%llu%s",
    393                      secs + 365ULL * 24 * 60 * 60,
    394                      dash);
    395     grants ("an expiration moved a year forward",
    396             forged,
    397             WEBSITE,
    398             sizeof (ca4),
    399             ca4,
    400             false);
    401     GNUNET_free (forged);
    402     GNUNET_free (val);
    403   }
    404 
    405   fprintf (stderr,
    406            "-- the website is inside the MAC --\n");
    407   val = mint_default ();
    408   grants ("presented for a different site",
    409           val,
    410           "https://example.com/premium/other",
    411           sizeof (ca4),
    412           ca4,
    413           false);
    414   grants ("presented for a different host",
    415           val,
    416           "https://evil.example.net/premium/article",
    417           sizeof (ca4),
    418           ca4,
    419           false);
    420   grants ("presented for a different scheme",
    421           val,
    422           "http://example.com/premium/article",
    423           sizeof (ca4),
    424           ca4,
    425           false);
    426   GNUNET_free (val);
    427 
    428   fprintf (stderr,
    429            "-- the client address is inside the MAC --\n");
    430   /* This is what stops a paid cookie from being passed around, and it
    431      is why get_client_address() has to be right about who the client
    432      is: whoever the address says paid, paid for everyone at it. */
    433   val = mint_default ();
    434   grants ("presented from a neighbouring address",
    435           val,
    436           WEBSITE,
    437           sizeof (ca4b),
    438           ca4b,
    439           false);
    440   grants ("presented from an address of a different family",
    441           val,
    442           WEBSITE,
    443           sizeof (ca6),
    444           ca6,
    445           false);
    446   grants ("presented from no address at all",
    447           val,
    448           WEBSITE,
    449           0,
    450           "",
    451           false);
    452   GNUNET_free (val);
    453   /* An IPv6 client is not a special case, just a longer one. */
    454   val = mint (WEBSITE,
    455               hour,
    456               sizeof (ca6),
    457               ca6);
    458   grants ("an IPv6 cookie for its own address",
    459           val,
    460           WEBSITE,
    461           sizeof (ca6),
    462           ca6,
    463           true);
    464   grants ("an IPv6 cookie for a truncation of it",
    465           val,
    466           WEBSITE,
    467           sizeof (ca4),
    468           ca6,
    469           false);
    470   GNUNET_free (val);
    471 
    472   fprintf (stderr,
    473            "-- malformed values are refused, not parsed --\n");
    474   /* Every one of these is a string an attacker can put in a Cookie
    475      header, and between them they take every exit check_cookie() has
    476      before it gets as far as comparing MACs: no separator at all, a
    477      seconds field that is not a bare decimal count ending at the
    478      separator, and a hash that does not decode. */
    479   refuses ("the empty value",
    480            "");
    481   refuses ("a value with no '-' at all",
    482            "deadbeef");
    483   refuses ("a '-' with no seconds before it",
    484            "-ABCDEF");
    485   refuses ("a non-numeric prefix",
    486            "soon-ABCDEF");
    487   {
    488     unsigned long long secs;
    489     char *bad;
    490 
    491     /* A well-formed, unexpired prefix followed by something that is
    492        not the base32 encoding of a hash: the length check and the
    493        alphabet check both live in string_to_data(). */
    494     secs = (unsigned long long)
    495            (GNUNET_TIME_timestamp_to_s (hour));
    496     GNUNET_asprintf (&bad,
    497                      "%llu-",
    498                      secs);
    499     refuses ("a prefix with an empty hash",
    500              bad);
    501     GNUNET_free (bad);
    502     GNUNET_asprintf (&bad,
    503                      "%llu-!!!!!!!!",
    504                      secs);
    505     refuses ("a hash outside the base32 alphabet",
    506              bad);
    507     GNUNET_free (bad);
    508     GNUNET_asprintf (&bad,
    509                      "%llu-ABCDEFGH",
    510                      secs);
    511     refuses ("a hash of the wrong length",
    512              bad);
    513     GNUNET_free (bad);
    514   }
    515   {
    516     /* A cookie of ours, respelled.  These are not malformed values in
    517        the sense above -- every one of them decodes to the same seconds
    518        and the same hash as a value we really issued, so a parser that
    519        accepts them accepts a second, third and fourth spelling of one
    520        live credential.  That is what a bare `sscanf ("%llu-")' did: it
    521        took a leading '+', leading whitespace and leading zeros, and its
    522        trailing '-' verified nothing, so anything at all could sit
    523        between the seconds and the separator.  A cookie has exactly one
    524        spelling or the set of strings that grant access is not the set
    525        we minted. */
    526     char *good = mint_default ();
    527     const char *gdash = strchr (good,
    528                                 '-');
    529     char *bad;
    530 
    531     GNUNET_assert (NULL != gdash);
    532     GNUNET_asprintf (&bad,
    533                      "+%s",
    534                      good);
    535     refuses ("a live cookie with a '+' before the seconds",
    536              bad);
    537     GNUNET_free (bad);
    538     GNUNET_asprintf (&bad,
    539                      " %s",
    540                      good);
    541     refuses ("a live cookie with a space before the seconds",
    542              bad);
    543     GNUNET_free (bad);
    544     GNUNET_asprintf (&bad,
    545                      "0%s",
    546                      good);
    547     refuses ("a live cookie with a leading zero on the seconds",
    548              bad);
    549     GNUNET_free (bad);
    550     GNUNET_asprintf (&bad,
    551                      "%.*sx%s",
    552                      (int) (gdash - good),
    553                      good,
    554                      gdash);
    555     refuses ("a live cookie with junk between seconds and '-'",
    556              bad);
    557     GNUNET_free (bad);
    558     GNUNET_asprintf (&bad,
    559                      "99999999999999999999999%s",
    560                      gdash);
    561     refuses ("a seconds count that does not fit",
    562              bad);
    563     GNUNET_free (bad);
    564     GNUNET_free (good);
    565   }
    566   {
    567     char *val2;
    568     char *swapped;
    569     const char *dash;
    570     unsigned long long secs;
    571 
    572     /* A syntactically perfect value whose hash simply is not ours:
    573        the last exit, the one the MAC comparison itself takes. */
    574     val = mint_default ();
    575     val2 = mint (WEBSITE,
    576                  hour,
    577                  sizeof (ca4b),
    578                  ca4b);
    579     dash = strchr (val2,
    580                    '-');
    581     GNUNET_assert (NULL != dash);
    582     GNUNET_assert (1 ==
    583                    sscanf (val,
    584                            "%llu-",
    585                            &secs));
    586     GNUNET_asprintf (&swapped,
    587                      "%llu%s",
    588                      secs,
    589                      dash);
    590     refuses ("another client's hash under our own timestamp",
    591              swapped);
    592     GNUNET_free (swapped);
    593     GNUNET_free (val2);
    594     GNUNET_free (val);
    595   }
    596 
    597   fprintf (stderr,
    598            "-- -g scopes the cookie to the whole deployment --\n");
    599   /* With PH_global_cookie the website drops out of the KDF, so one
    600      cookie is good everywhere paivana serves.  That is the point of
    601      the switch; what matters is that it is symmetric, i.e. that
    602      flipping it invalidates the cookies minted under the other
    603      setting rather than silently widening or narrowing access. */
    604   PH_global_cookie = 1;
    605   val = mint (WEBSITE,
    606               hour,
    607               sizeof (ca4),
    608               ca4);
    609   grants ("-g: a cookie minted elsewhere on the site",
    610           val,
    611           "https://example.com/other/article",
    612           sizeof (ca4),
    613           ca4,
    614           true);
    615   grants ("-g: still bound to the client address",
    616           val,
    617           "https://example.com/other/article",
    618           sizeof (ca4b),
    619           ca4b,
    620           false);
    621   PH_global_cookie = 0;
    622   grants ("-g cookie presented after -g was turned off",
    623           val,
    624           WEBSITE,
    625           sizeof (ca4),
    626           ca4,
    627           false);
    628   GNUNET_free (val);
    629   val = mint (WEBSITE,
    630               hour,
    631               sizeof (ca4),
    632               ca4);
    633   PH_global_cookie = 1;
    634   grants ("non-global cookie presented after -g was turned on",
    635           val,
    636           WEBSITE,
    637           sizeof (ca4),
    638           ca4,
    639           false);
    640   PH_global_cookie = 0;
    641   GNUNET_free (val);
    642 
    643   fprintf (stderr,
    644            "-- the paivana_id --\n");
    645   {
    646     struct PAIVANA_Nonce nonce;
    647     struct PAIVANA_Nonce nonce2;
    648     static const uint8_t nonce_bytes[16] = {
    649       0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
    650     };
    651     /* Computed independently from the same definition the paywall page
    652        implements (src/frontend/paywall.js makePaivanaId()):
    653          base64url (SHA-256 (nonce || website || '\0' || be64 (usec)))
    654        with the padding stripped, prefixed by the expiration in seconds.
    655        Both ends derive this from their own copy of the inputs and never
    656        exchange it, so the two implementations agreeing is the whole
    657        protocol: a change on either side that this vector does not
    658        survive means every order is created under an id the other side
    659        will not look for.  Note that the client hashes the expiration,
    660        which is why the pay endpoint cannot re-derive it and has to take
    661        the client's word for it (bounded by the contract). */
    662     static const char *want =
    663       "1700000000-gAGOp3pVpQq9VTA7P0cYqqKTiFGLxC_9FLZZXkmZwGc";
    664     char *id;
    665     char *id2;
    666 
    667     /* memcpy rather than an initialiser list: the nonce is hashed as
    668        the bytes it is in memory, and a `uint32_t val[4]' written as
    669        integers would give a different digest on a big-endian host. */
    670     memcpy (&nonce,
    671             nonce_bytes,
    672             sizeof (nonce));
    673     id = PAIVANA_HTTPD_compute_paivana_id (past,
    674                                            WEBSITE,
    675                                            &nonce);
    676     if (0 != strcmp (id,
    677                      want))
    678     {
    679       fprintf (stderr,
    680                "FAIL: paivana_id is `%s', want `%s'\n",
    681                id,
    682                want);
    683       failures++;
    684     }
    685     else
    686     {
    687       fprintf (stderr,
    688                "  ok: paivana_id matches the paywall page's derivation\n");
    689     }
    690     id_is_wellformed ("the shape of a paivana_id",
    691                       id);
    692 
    693     /* All three inputs have to reach the digest. */
    694     id2 = PAIVANA_HTTPD_compute_paivana_id (hour,
    695                                             WEBSITE,
    696                                             &nonce);
    697     differ ("a different expiration gives a different id",
    698             id,
    699             id2);
    700     GNUNET_free (id2);
    701     id2 = PAIVANA_HTTPD_compute_paivana_id (past,
    702                                             "https://example.com/premium/other",
    703                                             &nonce);
    704     differ ("a different website gives a different id",
    705             id,
    706             id2);
    707     GNUNET_free (id2);
    708     memcpy (&nonce2,
    709             nonce_bytes,
    710             sizeof (nonce2));
    711     nonce2.val[3] ^= 1;
    712     id2 = PAIVANA_HTTPD_compute_paivana_id (past,
    713                                             WEBSITE,
    714                                             &nonce2);
    715     differ ("a different nonce gives a different id",
    716             id,
    717             id2);
    718     GNUNET_free (id2);
    719     /* The whole website string reaches the digest, terminating NUL
    720        included -- not some prefix of it. */
    721     id2 = PAIVANA_HTTPD_compute_paivana_id (past,
    722                                             WEBSITE "x",
    723                                             &nonce);
    724     differ ("a website that extends another gives a different id",
    725             id,
    726             id2);
    727     GNUNET_free (id2);
    728     /* ...and it is a pure function, or the order and the redemption
    729        would never line up. */
    730     id2 = PAIVANA_HTTPD_compute_paivana_id (past,
    731                                             WEBSITE,
    732                                             &nonce);
    733     if (0 != strcmp (id,
    734                      id2))
    735     {
    736       fprintf (stderr,
    737                "FAIL: paivana_id is not deterministic: `%s' then `%s'\n",
    738                id,
    739                id2);
    740       failures++;
    741     }
    742     else
    743     {
    744       fprintf (stderr,
    745                "  ok: paivana_id is deterministic\n");
    746     }
    747     GNUNET_free (id2);
    748     GNUNET_free (id);
    749   }
    750 
    751   if (0 != failures)
    752   {
    753     fprintf (stderr,
    754              "%u check(s) failed\n",
    755              failures);
    756     return 1;
    757   }
    758   fprintf (stderr,
    759            "all checks passed\n");
    760   return 0;
    761 }