Files
dckc 3706e6de2c fixup! docs: webkeys — ids as bearer capabilities, kept out of URLs
The cache page now follows the paper's form (fragment + skeleton page),
with the XHR replaced by a Cap'n Web call taking the key in-band.

Generated-by: Claude Opus 5.5
2026-10-07 04:32:44 -05:00

5.6 KiB
Raw Permalink Blame History

webkeys

A web-key is an https URL that carries a permission: an unguessable random secret embedded in the URL, bearer-style. From Tyler Close's web-key: Mashing with permission paper (IEEE W2SP 2008) — also, the Waterken precursor code in the E-on-Java tree (kpreid/e-on-java). Written 2026-10-07 for diigo-cache's id-in-URL problem.

why passwords fail and URLs can carry permission

Web URLs are the one global identifier, but they are only supposed to identify; permission is supposed to ride ambient credentials (username/password, cookies). Close's paper argues that split is the root of several ills: sharing means sharing credentials, representations must encode auth (breaking orthogonality), the Same Origin Policy exists to plug the resulting hole, and XSRF happens because "client authentication … does not establish which of the client's permissions the client intends to exercise".

The web-key fix: put the permission in the URL, as a random secret. Possession of the URL is the permission (bearer; no MAC, no signatures). The server stores the secret and only it can tell a web-key from garbage — so an attacker's search is online guesswork, never offline cracking.

the five conventions (from the paper)

  • The key is a random bit string, never algorithmically derived. The paper specs 64 bits: at ≤ 1M permissions and 1 request/ms, ~292 years to have a 50% chance of guessing even one. (E's CapTP used 160-bit swiss numbers; diigo-cache's DO ids are 256 bits.)

  • YURL32 encoding: lowercase a–z + 2–7, 5 bits per char, URL-safe by construction. 64 bits → 13 chars; 128 → 26; 160 → 32; 256 → 52.

  • The key goes in the fragment. The paper's examples:

    https://www.example.com/app/#mhbqcmmva5ja3
    https://mail.example.com/user123/#mhbqcmmva5ja3
    

    This placement is the paper's central move. RFC 2616 §14.36 requires browsers to omit the fragment from Referer; and fragments never reach the server at all. So the key leaks neither to other sites nor to server/proxy logs — and Close is candid that this exploits a quirk: HTTP's Referer behaves like a dynamically scoped language, and the fragment is the one slot its leak misses.

  • Dereferencing is client-side (skeleton page + XHR): the server serves a skeleton page, identical for every web-key of that resource and cacheable forever; its JavaScript reads document.location, moves the key out of the fragment, and issues an XHR carrying the key (as a query param) to fetch the actual representation. Two round trips on first visit, one ever after.

  • Identity without authority: the swissHash discipline — E's hash chain (swissBase → swissNum → swissHash) shows how to log safely — the hash "represents the identity of the object without providing any authority" (SwissTable.java:106-108); the chain is one-way on the assumed infeasibility of the "archash". Failure messages leak "no more than a swissHash".

  • Attenuation by minting, revocation by dropping: a new key is minted to narrow a view (registered, not algorithmically derived — Amoeba's derivable sub-capabilities hid excess-grant bugs); revocation drops the table entry; later holders "resolve to broken".

the precursor style: key in the path segment (E-on-Java tree)

Before the fragment form, Waterken's capabilities put the secret in a path segment: captp://*vatID@searchpath/swissStr[#expirationDate] (EARL.java:37), with the host authenticated by public-key fingerprint. The AMP web-calculus client POSTs to cap + "?mid=" + mid (AMP.java:83) — the mid is a 128-bit freshness nonce, not an authenticator. Fragments there carry non-secrets only (#make, #expirationDate). The 2008 paper moved the secret into the fragment and the pattern got its standard form.

what this means for diigo-cache: a webkey example

https://diigo-cache.example.com/cache/#5y7kq2mhbqcmmva5ja3x9w2f4e
  • …/cache/ — the address, non-secret, fine in logs, identical for every cache (skeleton-cacheable)
  • the fragment — the 256-bit DO id, 52 YURL32 chars (a–z2–7 only), minted by newUniqueId(), never derived from anything; all the authority, none of the log-trail
  • diigo-cache's ids are already webkeys in substance (bearer secrets); the cache page now follows the paper's form — /cache/#<key>, a skeleton page, its script passing the key to the server — with the XHR replaced by a Cap'n Web call that takes the key in-band, as a method argument (api.cache(key)), returning a capability. The forms elsewhere still carry keys in POST bodies (same leak profile as the fragment). A hash of the id would stand in wherever log-correlation is wanted (the swissHash discipline; shashKey, whose prefix is also the on-screen fingerprint). The bug that prompted this note: a cache id briefly rode a query string (/?cacheId=…) — the leaky spot the pattern exists to prevent. It recurred once as a path segment (/_publish/<cacheId>); publish now takes the id in its POST body.

where this came from

  • web-key: Mashing with permission — Close's paper (IEEE W2SP 2008); paper PDFs, reviewer comments, and the W2SP publication are linked from that page
  • the Waterken code in E-on-Java (kpreid/e-on-java): net/vattp/data/EARL.java, net/captp/tables/SwissTable.java, org/waterken/url/amp/AMP.java, org/waterken/uri/Base32.java, org/erights/e/extern/persist/{SturdyRefMaker,SwissRetainer}.java, net/captp/jcomm/IdentityMgr.java