docs: webkeys — ids as bearer capabilities, kept out of URLs
Close's web-key pattern (W2SP 2008) and its Waterken precursors, applied to diigo-cache: cache ids are webkeys in substance; they travel in POST bodies, never query strings, and a one-way hash (swissHash discipline) stands in for them where log correlation is wanted. Generated-by: opencode (GLM)
This commit is contained in:
1 parent
cbf1435571
commit
c5e433cc9c
1 file changed
+100
+100
@@ -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/<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`
|
||||||
Reference in new issue
Block a user