diff --git a/docs/webkeys.md b/docs/webkeys.md new file mode 100644 index 0000000..7cfb215 --- /dev/null +++ b/docs/webkeys.md @@ -0,0 +1,100 @@ +# 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](https://waterken.sourceforge.net/web-key/) 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](../capsite-checklist.md).* +## 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 honest deviations, both known: keys travel in **POST bodies** + rather than fragment+XHR — same leak profile, simpler under a + form-driven UI with no skeleton page; and 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/`); publish now takes the id in its POST body. + +## where this came from + +- [web-key: Mashing with permission](https://waterken.sourceforge.net/web-key/) + — 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` \ No newline at end of file