Files
captoys/docs/webkeys.md
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

104 lines
5.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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](https://github.com/cloudflare/capnweb)
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](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`